====== 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 == Hardware related limitations == * 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 ''
'' 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 ===
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 ===
{
"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.
// 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).
// 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 ===
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 ===
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.
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);
}
=== 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 =====
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 ====
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 ====
class Vendor_Feature_Admin_Controller_Items extends FCom_Admin_Controller_Abstract
{
protected static $_permission = 'vendor/feature';
}
===== DB Migration =====
==== Suggested Usage ====
{
"modules": {
"Vendor_Feature": {
/*...*/
"migrate": "Vendor_Feature_Migrate",
/*...*/
}
}
}
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()'';