User Tools

Site Tools


fulleron:module

Fulleron Modules

This page describes standard steps of creating a module in Fulleron framework.

Conventions

Fulleron is built upon Buckyball library and inherently is not rigid in its structure or implementation. However, the following set of conventions is recommended for clean, uniform and maintainable application.

Primary goal

Achieve a maintainable balance between:

Abstract concepts
  • Separation of concerns: files should be separated by their role in app
  • Clear folder hierarchy: child entities should be under parent entities
  • Storage speed/latency: more folders/files and deeper structure affect performance
  • CPU/Storage limitation: optimize use of bytecode caching
Immediate developer needs
  • Files should be intuitively found in folder structure
  • Number of files open in IDE should be minimized
  • Number of total files should be minimized (avoid “rice code”)
  • Reduce number of files with the same name (not an issue in some IDEs)
  • Ease of debugging (resolving development errors)
  • Some local configuration files should be shared between repo copies
Future maintenance and flexibility
  • Ease of refactoring (renaming/moving)
  • Ease of debugging (finding and resolving production errors)
  • Internal extensibility (adding new features within module)
  • External extensibility (adding/changing functionality from other modules)
  • Core and market module files should never be changed by developer

Folder structure considerations

The basic question whether files should be physically grouped by role (code, templates, css/js, etc) or bundle (independent feature set) has been definitely settled for the latter. The grouping by role is performed logically by the application (classes autoload, collection of views, collection of <head> elements).

File content and naming considerations

Classes

Multiple classes in the same file?

While it is widely accepted to keep each class in a separate file, sometimes it makes more sense to keep many small classes within the same file.

Fulleron modules always load at least 1 class file, which contain a bootstrap method. If the module has a small bootstrap, model and controller, it makes sense to keep them in 1 file, so the filesystem will not have to waste time on loading many small files.

NOTE: This should be used only during development (scaffolding new classes) or for very small modules to avoid wasting time on finding classes.

Separate application areas (admin/frontend)

In most cases bootstrap and configuration will differ significantly between admin and frontend areas, so it doesn't make sense to load the other part. This leads to splitting bootstrap files into Admin.php and Frontend.php and folders Admin/ and Frontend/ which contain all the files related to each respective area.

The bootstrap files can be named FeatureAdmin.php and FeatureFrontend.php to avoid multiple open files with the same name in IDE. The class names should stay Vendor_Feature_Admin and Vendor_Feature_Frontend. The files will be found outside of autoload from manifest.

If the module is small it might not make sense to separate classes/files, and same bootstrap file/class can cater to all areas.

Templates

Reasons to keep number of template files small
  • It becomes hard to know what is used when and where (large picture)
  • Performance greatly suffers with multiple files
Reasons to split templates into smaller chunks
  • No need to override huge template for a small change, opening up for issues with future upgrades
  • Easier to read and understand a specific file (small picture)

File structure

Anatomy of Fulleron application

index.php         # Frontend entry point
.htaccess
FCom/             # Core component modules. Can be located in shared location.
    buckyball/    # Buckyball general purpose library
        com/      # Buckyball components
        plugins/  # Buckyball standard plugins
    Core/         # Main module, initializes environment, starts the request
admin/
    index.php     # Admin UI entry point
    .htaccess
api/
    index.php     # Remote API entry point.
    .htaccess
tests/
    index.php     # Tests entry point. Limited to local IP by default
    .htaccess
local/            # local custom modules
market/           # modules installed from market
media/            # web accessible assets
storage/          # can be outside of web root
    .htaccess     # optionally protected from web access
    cache/
    config/
        db.php
        local.php
    import/
    export/
    log/
    phptal/
    session/

Anatomy of a module

local/
    Vendor/
        manifest.(json|php)
        Feature/
            manifest.(json|php)
            Admin/
                Controller/
                    Things.php
                views/
                    settings/
                        Vendor_Feature.php
                    things/
                        form-main.php
            Frontend/
                css/
                js/
                views/
                Controller.php
            Model/
                Thing.php
            Admin.php
            Frontend.php
            Migrate.php

Manifests

  • Accepted manifest files are PHP or JSON files, which describe module aspects that are required to correctly identify, validate and load modules.
  • A manifest file can describe a single or multiple modules.
  • Modules within the same manifest can be enabled or disabled independently.
  • Order of multiple modules within the same manifest is insignificant.
  • Manifests can declare other folders where Fulleron should be scanning for modules (useful for vendor manifests).

Manifest object structure

Sample from FCom

manifest.php
<?php return array(
  'modules' => array(
      'FCom_Admin_DefaultTheme' => array(
          'version' => '0.1.0',
          'root_dir' => 'Admin',
          'bootstrap' => array('file'=>'DefaultTheme.php', 'callback'=>'FCom_Admin_DefaultTheme::bootstrap'),
          'depends' => array('FCom_Core'),
          'description' => "Default admin theme",
      ),
  ),
);

Sample from local dev

manifest.json
{
    "modules": {
        "Unirgy_Maps": {
            "version": "0.1.0",
            "depends": ["FCom_Core"],
            "load_after": ["SomeModule"],
            "migrate": "Unirgy_Maps_Migrate",
            "bootstrap": {"file":"MapsFrontend.php", "callback":"Unirgy_Maps_Common::bootstrap"}
            "areas": {
                "FCom_Frontend": {
                    "bootstrap": {"file":"MapsFrontend.php", "callback":"Unirgy_Maps_Frontend::bootstrap"}
                },
                "FCom_Admin": {
                    "bootstrap": {"file":"MapsAdmin.php", "callback":"Unirgy_Maps_Admin::bootstrap"}
                }
            }
        }
    }
}

The areas parameter will override manifest with child nodes of the current area.

Areas

The website is logically split into areas of concern

Areas available by default:

  • FCom_Frontend
  • FCom_Admin
  • FCom_Cron
  • FCom_Api

To avoid wasting request time on bootstrapping initialization that is not required in every area, the bootstrap files are split in the way that makes sense for each specific module.

If a module has minimal or doesn't have any UI, the bootstrap can be the same for all areas. Otherwise, it is recommended to keep initialization of Admin controllers, events and views separate from Frontend, etc. The common initialization which is required for all areas, including areas unknown at the time of module development, should be isolated as a Vendor_Feature_Common bootstrap file, and called from within other areas' bootstraps.

Area is defined by entry point script (index.php) and is accessible via BApp::get('area')

Examples of custom areas:

  • Vendor_Dealer - 3rd party dealer control panel

Bootstrap files

Bootstrap files contain a class with at least one method (public static function bootstrap()), which initiates declaration of all system hooks the module injecting into application.

NOTE: Because the bootstrap method will be called always when the method is active, it should execute and exit quickly. Only system hook declarations should be called from bootstrap method.

On a slow dev server with debug log enabled the bootstrap should take less than 1ms and maximum 2-3ms. All the profiling is available in DEBUG mode on every page.

All the bootstrap actions are logged, including in which module they happened, to help with debugging.

System hook declarations

Routes

Routes declare which action should be taken on match of a specific pattern of request method(s) and path(s).

The 1st parameter is route pattern, 2nd is the callback, and 3rd optional parameters. Fulleron has a special notation for callbacks, used in events and routes, where class and method are separated by dot (.) This means that a method of a singleton will be executed. Alternative syntax is (→).

Controller actions are prepended by action_ prefix to separate from utility methods and avoid calling unwanted callbacks from web.

Vendor/Feature/Feature.php
// Example:
    BFrontController::i()
      ->route('GET /feature', 'Vendor_Feature_Frontend_Controller.index')
      ->route('GET|POST /feature/.action', 'Vendor_Feature_Frontend_Controller')
 
// Possibilities:
      // Parameter is optional if last. 
      // Action methods other than GET should have suffix crud__POST, crud__PUT, crud__DELETE
      ->route('GET|POST|PUT|DELETE /model/:id', 'Rest_Controller_Model.crud')
 
      // Multiple parameters, specific method, no need for prefix
      ->route('POST /model/:id/child/:child_id', 'Rest_Controller_Model.child_post')
 
      // Required parameter, will not match without
      ->route('GET /article/!id', 'Article_Controller.view')
 
      // Wildcard, can include slashes
      // Multiple handles of the same route can be declared, 
      // will be performed until not forwarded from within action
      ->route('GET /*category', 'FCom_Catalog_Frontend_Controller_Categories.view')
      ->route('GET /*product', 'FCom_Catalog_Frontend_Controller_Products.view')
      ->route('GET /*category/:product', 'FCom_Catalog_Frontend_Controller_Products.view')
      ->route('GET /*page', 'FCom_Cms_Frontend_Controller_Page.view')
 
      // Call any controller action, specified by parameter
      ->route('GET|POST /dealer/.action', 'Dealer_Frontend_Controller')
 
      // Regexp route match (starts with ^)
      ->route('^(GET|POST) /pattern-([^/]+)/?$', 'Some_Controller.action')
  ;

Views

Each module contains all its views (templates) within module folder. Views are separated by application area (Admin, Frontend).

Vendor/Feature/Feature.php
    // register all views (templates) from folder Frontend/views relative to module root dir
    BLayout::i()->addAllViews('Frontend/views');

The views are accessible via BLayout::i()→view('view/name') or $this→view('view/name') from within another view.

The previous example will reference view Frontend/views/view/name.php.

The file extension will define which renderer will be used to render the template file. .php is default and the other currently available is .zpt.html which is Zope Template format, implemented using PHPTAL library. Additional renders can be added as plugins.

PHPTAL is currently the default renderer for CMS pages and blocks, with some custom integrations with Fulleron.

It is also possible to decare individual views instantiated not from default class (other doc scope).

Event Observers

Vendor/Feature/Feature.php
public static function bootstrap()
{
//...
    BPubSub::i()
// Usual syntax
        ->on('Originating_Class::method.specific', 'Callback_class::method')
// Singleton instance method callback
        ->on('FCom_Catalog_Model_Product::afterSave', 'MyBootstrapClass.onProductSaveAfter')
    ;
}
 
public function onProductSaveAfter($args)
{
    $product = $args['model'];
    // do something with $product model
}

Themes/Layouts

Vendor/Feature/Feature.php
public static function bootstrap()
{
    // add/update module related layouts
    BLayout::i()->afterTheme('MyBootstrapClass::layout');
}
 
public static function layout()
{
    BLayout::i()->addLayout(array(
// update base layout initialization, which is loaded for most pages
        'base' => array(
            array('view', 'head', 'do'=>array(
                array('js', '{MyModule}/js/jquery.is.cool.js'), // declare a JS source from my module
                array('css', '{MyModule}/css/styles.css'), // my styles
                array('icon', '{MyModule}/favicon.ico'), // my app icon
                array('meta', 'meta-name', 'meta-value', true) // set http-equiv meta
                array('remove', 'js, '{AnotherModule}/js/file.js'), // remove script that was declared in another module
 
                array('addTitle', 'My Great App'), // add to existing title
                array('setTitleSeparator', ' | '), 
            )),
            array('view', 'nav', 'do'=>array(
                array('addNav', 'my/url', 'Label for my url'), // add navigation item for my url
            )),
        ),
// update a specific page identified by url. The convention is if it starts with slash it's a page, otherwise it has a special use
        '/my/url' => array(
            array('layout', 'base'), // load base layout before anything
            array('view', 'nav', 'do'=>array(
                array('setNav', 'my/url'), // set navigation active for current url
            )),
            array('hook', 'main', 'views'=>array('my/view')), // add my page view to main content hook
        ),
    ));
}

Class overrides

All classes that inherit from BClass or BModel can be overridden, and any use that starts with OriginalClass::i() will call the new class singleton.

AugmentedClass.php
class AugmentedClass extends OriginalClass
{
 
}
 
  BClassRegistry::i()->overrideClass('OriginalClass', 'AugmentedClass');

NOTE: If you wish to have IDE autocompletion, add this method to your class:

OriginalClass.php
    /**
    * Shortcut to help with IDE autocompletion
    *
    * @return OriginalClass
    */
    public static function i($new=false, array $args=array())
    {
        return BClassRegistry::i()->instance(__CLASS__, $args, !$new);
    }

Other Initializations

Layouts

Directives

// layout directives structure:
// first item is directive type, 2nd is item reference, after that are commands
array(
// these are the available directives:
    // set root view. this view will be the one loaded on response, and call other views
    array('root', 'some/view'),
 
    // load another layout
    array('layout', 'base'),
 
    // add views to a hook, in a view: < ?php echo $this->hook('main') ? >
    array('hook', 'main', 'views'=>array('my/view', 'another/view')),
 
    // perform a method or set variable to a view
    array('view', 'my/view', 
        'set'=>array(
            'var1' => 'value1',
            'var2' => 'value2',
        ),
        'do'=>array(
            array('method1', 'arg1', 'arg2'),
            array('method2', 'arg3', 'arg4'),
        ),
    ),
 
    // perform an arbitrary callback
    array('callback', 'SomeClass::method'),
)

Models

Vendor/Feature/Model/Item.php
class Vendor_Feature_Model_Item extends FCom_Core_Model_Abstract
{
// declare table name
    protected static $_table = 'vendor_feature_item';
// declare original class for event names, in case there's an override
    protected static $_origClass = __CLASS__;
}

Controllers

Frontend

Vendor/Feature/Frontend/Controller/Items.php
class Vendor_Feature_Frontend_Controller_Items extends FCom_Frontend_Controller_Abstract
{
    public function action_index()
    {
        $this->layout('/feature');
    }
 
    public function action_index__POST()
    {
        //...
        BResponse::i()->redirect(BApp::href('/feature'));
    }
 
    public function action_json()
    {
        $request = BRequest::i()->get();
        $response = array(/*...*/);
        BResponse::i()->json($response);
    }
 
    public function action_json__POST()
    {
        $request = BRequest::i()->json();
        $response = array(/*...*/);
        BResponse::i()->json($response);
    }
}

Admin

Vendor/Feature/Admin/Controller/Items.php
class Vendor_Feature_Admin_Controller_Items extends FCom_Admin_Controller_Abstract
{
    protected static $_permission = 'vendor/feature';
}

DB Migration

Suggested Usage

manifest.json
{
  "modules": {
    "Vendor_Feature": {
      /*...*/
      "migrate": "Vendor_Feature_Migrate",
      /*...*/
    }
  }
}
Vendor/Feature/Model/Thing.php
<?php
class Vendor_Feature_Model_Thing extends FCom_Core_Model_Abstract
{
    protected static $_table = 'vendor_thing';
 
    /*...*/
 
    public static function install()
    {
        BDb::run("
CREATE TABLE IF NOT EXISTS ".static::table()." (
/* ... */
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
        ");
    }
}
Vendor/Feature/Migrate.php
<?php
class Vendor_Feature_Migrate extends BClass
{
    public function run()
    {
        BMigrate::install('0.1.0', array($this, 'install'));
        BMigrate::upgrade('0.1.0', '0.1.1', array($this, 'upgrade_0_1_1'));
    }
 
    public function install()
    {
        Vendor_Feature_Model_Thing::i()->install();
        BDb::run("
//...  
        ");
    }
 
    public function upgrade_0_1_1() 
    {
        BDb::run("
//...  
        ");  
    }
}

The recommended practice is to keep one install method for the latest version (for new installations), and multiple upgrade methods (for existing installations)

If you run any ORM operations within migration scripts after updating table structure, reset DDL cache: BDb::ddlClearCache();

fulleron/module.txt · by unirgy