This page describes standard steps of creating a module in Fulleron framework.
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.
Achieve a maintainable balance between:
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).
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.
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.
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/
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
<?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", ), ), );
{ "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.
The website is logically split into areas of concern
Areas available by default:
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:
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.
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.
// 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') ;
Each module contains all its views (templates) within module folder. Views are separated by application area (Admin, Frontend).
// 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).
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 }
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 ), )); }
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.
class AugmentedClass extends OriginalClass { } BClassRegistry::i()->overrideClass('OriginalClass', 'AugmentedClass');
NOTE: If you wish to have IDE autocompletion, add this method to your class:
/** * Shortcut to help with IDE autocompletion * * @return OriginalClass */ public static function i($new=false, array $args=array()) { return BClassRegistry::i()->instance(__CLASS__, $args, !$new); }
// 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'), )
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__; }
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); } }
class Vendor_Feature_Admin_Controller_Items extends FCom_Admin_Controller_Abstract { protected static $_permission = 'vendor/feature'; }
{ "modules": { "Vendor_Feature": { /*...*/ "migrate": "Vendor_Feature_Migrate", /*...*/ } } }
<?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; "); } }
<?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()
;