Доброго утра и хорошего всем дня! Вчера, читая Хабр, наткнулся на статью про создание модуля для Magento. Это известная в мире система от наших украинских коллег для создания Интернет-магазина. Читая статью, меня одолевал один единственный вопрос — зачем так усложнять? Такой же вопрос я постоянно задаю себе в сложные моменты разработки, разворачивая наш корабль на курсы простоты и эффективности. Помните, что простота еще не означает «говнокода» или непрофессионализма. Как говорится, все гениальное просто. Давайте посмотрим, что требуется для того, чтобы создать шестеренку для cogear².Обратимся к шестеренке jQuery.
<?php
/**
* jQuery
*
* @author Dmitriy Belyaev <admin@cogear.ru>
* @copyright Copyright © 2011, Dmitriy Belyaev
* @license http://cogear.ru/license.html
* @link http://cogear.ru
* @package jQuery
* @subpackage
* @version $Id$
*/
class jQuery_Gear extends Gear {
protected $name = 'jQuery';
protected $description = 'Add jQuery support to Cogear';
}
Это минимально необходимый код для того, чтобы система опознала шестеренку и могла работать с ней.Заметили на картинке папку js, в которой лежит последняя версия популярного фреймворка? Каким образом движок понимает это и подгружается скрипты? Очень просто. Обратите внимание на то, что ваш класс наследуется от оригинальной единой и неделимой Gear.
Два самых важных класса в движке — это Cogear и Gear
.
Шестеренка — наше все
Предлагаю заглянуть в класс шестеренки и пройти по всем ее свойствам и полям. Файл лежит по адресу /engine/Core/Gear.php, но как мы помним, использовать префикс Core_ в названии классов ядра совсем не обязательно.Начинаем.
<?php
/**
* Gear
*
* @author Dmitriy Belyaev <admin@cogear.ru>
* @copyright Copyright © 2011, Dmitriy Belyaev
* @license http://cogear.ru/license.html
* @link http://cogear.ru
* @package Core
* @subpackage
* @version $Id$
*/
abstract class Gear extends Cogearable{
Почему класс абстрактный? Чтобы от него нельзя было создать объект. Класс только для наследования в реальных шестеренках. Что за наследуемый класс Cogearable? Очень просто. Чтобы внутри класса можно было писать
$this->db->…;
вместо$cogear = getInstance();
$cogear->db->…;
и был сделан такой класс./**
* Gear name
* @var string
*/
protected $name;
/**
* Gear description
* @var string
*/
protected $description;
/**
* Gear version
*
* By default it equals Cogear version
*
* @var string
*/
protected $version = COGEAR;
Имя, описание — на английском. Потом пропустим через перевод. Версия — по-умолчанию для классов ядра = версии движка. В своих шестеренках вести версионность будете сами. Помните, что любые изменение должны быть зафиксированы версией. Пусть миноритарной в духе 0.1.2, но должны. Еще лучше вести файл changelog.md (в формате Textile) в корне шестеренки./**
* Core version
*
* By default it equals Cogear version
* * @var string
*/
protected $core = COGEAR;
Версия ядра, с которой совместима данная шестеренка./**
* Gear type
*
* @var int
*/
protected $type = Gear::CORE;
…
const CORE = 0;
const MODULE = 1;
const THEME = 2;
…
Тип шестеренки./**
* Package
*
* @var string
*/
protected $package = 'Core';
Да, теперь шестеренки можно объединять в пакеты. Это будет заметно при группировке шестернок в админке./**
* Gear authors name
*
* @var string
*/
protected $author = 'Dmitriy Belyaev';
/**
* Gear email
*
* Contact email to resolve everything
* @var string
*/
protected $email = 'admin@cogear.ru';
/**
* Gear website
*
* @var string
*/
protected $site = 'http://cogear.ru';
Данные о разработчике и продукте. Важно, что мы наследуемся от базового класса. Я один раз написал свои данные в базовом классе, а поскольку все остальные классы ядра наследуются от него, то все шестеренки автоматически получают те же самые данные. Использовали принцип наследования в очень удачных целях.
/**
* Path to class file
*
* @var string
*/
protected $path;
/**
* Directory where gear is located
* @var string
*/
protected $dir;
/**
* Relative path to folder with class
*
* @var string
*/
protected $folder;
Абсолютные и относительные пути к шестеренке.
/**
* Order in gear stack
*
* Value can be positive or negative to load after or before other gears.
*
* @var int
*/
protected $order = 0;
Очень важный момент. Порядок загрузки шестеренки. Может быть отрицательным. Скрипты и стили шестеренок загружаются согласно их порядку. Также если вы в своей шестеренке пытаетесь использовать другу при ее инициализации, помните, что она должна быть загружена ранее.
Давайте заглянем в сам Cogear.php, чтобы понять о чем речь:
$this->sortGears();
foreach ($this->gears as $name => $gear) {
$gear->init();
}
Сначала шестеренки сортируются, а потом уже вызывается их инициализация. Если не поняли, вспомните, когда дело до этого дойдет, и вернетесь к данному материалу.
Далее опять в шестеренку смотрим.
/**
* Class reflection
*
* Metaclass that stores all the info about current class
*
* @var ReflectionClass
*/
protected $reflection;
Еще одно полезное дело из SPL. Рефлекция — это информация о классе, его отражение. Очень удобный объект, из которого можно узнать много полезного о текущем классе.Что важно, если мы имеем базовый класс от которого наследуемся и хотим, чтобы в базовом классе определялось имя наследуемого, то нам не помогут ни __CLASS__, ни get_class(). То есть у нас есть класс Db_ORM, а мы от него Db_Item отнаследовали. И мы хотим внутри одного из методов Db_ORM получить имя текущего дочернего класса. Вышеназванные методы дадут нам только имя текущего класса Db_ORM, а вот рефлекция, напротив, даст нам как и полагается Db_Item.
/**
* Gear name
*
* How does it stored in $cogear->gears->$name
*
* @param string
*/
protected $gear;
/**
* Info about gear file
*
* @var SplFileInfo
*/
protected $file;
Что важно, переменная $file также содержит объект из SPL класса SplFileInfo. Он поможет нам узнать, например, когда файл шестеренки был изменен, и так далее.
/**
* Simple uri name
* It can be set in configuration, but if empty — will be default gear_name
*
* @var string
*/
protected $base;
Мы можем менять базовый Url шестеренки. Хотите, чтобы шестеренка admin располагалась по адресу super_secret_control_panel? Просто впишите новый базовый адрес здесь./**
* Gear settings
*
* @var Core_ArrayObject
*/
protected $settings = array();
Если нужны постоянные настройки — можно хранить их здесь. Также можно сделать массив в файле settings.php и положить его в корень шестеренки. Он считается в эту переменную автоматически./**
* If gear is requested by router
* @var boolean
*/
protected $is_requested;
Если данная шестеренка получила запроса от роутера./**
* Flag indicates if gear is active
*
* @var boolean
*/
public $active;
Если шестеренка включена, то данный флаг нам об этом скажет./**
* Required gears [version is optoinal]
*
* array(
* 'Phpinfo',
* // or with version
* 'Phpinfo 1.1',
* // or even with condition
* 'Phpinfo > 1.1',
* )
* @var array
*/
protected $required = array();
Запрос других шестеренок. Например, ваша шестеренка не работает без шестеренки Phpinfo, тогда пишете согласно инструкции в комментарии к переменной.Конструктор
/**
* Constructor
*/
public function __construct() {
$this->reflection = new ReflectionClass($this);
$this->getPath();
$this->getDir();
$this->getFolder();
$this->getGear();
$this->getBase();
$this->getSettings();
$this->file = new SplFileInfo($this->path);
}
Инициализация
/**
* Initialize
*/
public function init() {
$scripts = $this->dir . DS . 'js';
$styles = $this->dir . DS . 'css';
is_dir($scripts) && $this->assets->addScriptsFolder($scripts);
is_dir($styles) && $this->assets->addStylesFolder($styles);
$this->router->addRoute($this->base . ':maybe', array($this, 'index'));
$this->event('gear.init', $this);
}
Вот где скрипты и стили подключаются, а также задается базовый путь для роутера.Магия
/**
* Magic __get method
*
* @param string $name
* @return mixed
*/
public function __get($name) {
return isset($this->$name) ? $this->$name : parent::__get($name);
}
Магический метод для получения доступа к protected переменным. Читать можем, писать — нет. Также он отсылает на родительский Cogearable, сами знаете зачем.Инфо
/**
* Gear info
*
* @param string $var
* @return array
*/
public function info($var = NULL) {
if ($var) {
return isset($this->$var) ? $this->$var : NULL;
}
else
return array(
'name' => $this->name,
'gear' => $this->gear,
'base' => $this->base,
'description' => $this->description,
'version' => $this->version,
'package' => $this->package,
'type' => $this->type,
'author' => $this->author,
'email' => $this->email,
'url' => $this->url,
'path' => $this->path,
'dir' => $this->dir,
'folder' => $this->folder,
);
}
Информация о шестеренке.
Установка и далее по списку
/**
* Install
*/
public function install() {
}
/**
* Uninstall
*/
public function uninstall() {
}
/**
* Activate
*/
public function activate() {
}
/**
* Deactivate
*/
public function deactivate() {
}
/**
* Update gear
*/
public function update() {
}
Данные методы для каждой шестеренки индивидуальны. Пока что они пустуют.Базовые методы
/**
* Get Gear Path
*
* @return string
*/
protected function getPath() {
return $this->path = $this->reflection->getFileName();
}
/**
* Get Gear directory
*
* @return string
*/
protected function getDir() {
if (!$this->path)
$this->getPath();
return $this->dir = dirname($this->path);
}
/**
* Get Gear relative folder
*
* @return string
*/
protected function getFolder() {
if (!$this->dir)
$this->getDir();
$this->folder = str_replace(array(ROOT, DS), array('', '/'), $this->dir);
return self::normalizePath($this->dir);
}
/**
* Get Gear name
*
* @return string
*/
protected function getGear() {
return $this->gear ? $this->gear : $this->gear = Cogear::prepareGearNameFromClass($this->reflection->getName());
}
/**
* Get base name
*/
protected function getBase() {
$cogear = getInstance();
$base = str_replace('_', '/', strtolower($this->gear));
return $this->base ? $this->base : $this->base = $cogear->get($this->gear . '.base', $base);
}
/**
* Get gear options
*/
protected function getSettings() {
$this->settings = new Core_ArrayObject($this->settings);
if ($config = Config::read(find(basename($this->dir) . DS . 'settings' . EXT))) {
return $this->settings ? $this->settings->mix($config) : $this->settings = $config;
}
return NULL;
}
Мы вызывали их в конструкторе.
Помошники
Специальные статические методы./**
* Normalize relative path
*
* For example, under windows it look like \cogear\Theme\Default\, but wee need good uri to load css, js or anything else.
* It transorm that path to /cogear/Theme/Default/.
* @param string $path
* @return string
*/
public static function normalizePath($path) {
$path = str_replace(DS, '/', $path);
return $path;
}
/**
* Get gear name by path
*
* @param string $path
* @return string|boolean Gear name or FALSE if path is not correct.
*/
public static function getGearNameFromPath($path) {
foreach (array(SITE . DS . GEARS_FOLDER, GEARS, ENGINE) as $dir) {
if (strpos($path, $dir) !== FALSE) {
is_file($path) && $path = dirname($path);
$path = str_replace($dir, '', $path);
$path = trim($path, DS);
$pieces = explode(DS, $path);
$gear_folder = '';
foreach ($pieces as $piece) {
$gear_folder .= $piece . DS;
$gear_name = str_replace(DS, '_', trim($gear_folder, DS));
$gear_class = $gear_name . '_Gear';
if (file_exists($dir . DS . $gear_folder . DS . 'Gear' . EXT) && class_exists($gear_class)) {
return $gear_name;
}
}
}
}
return FALSE;
}
/**
* Prepare path
*
* Errors.index = Core/Errors/$dir/index
*
* @param string $name
* @param string $dir
* @param string $default
* @return string Path without file extension
*/
public static function preparePath($name, $dir = '', $default = 'index') {
if ($pieces = preg_split('#[\s><.-]#', $name, -1, PREG_SPLIT_NO_EMPTY)) {
if (sizeof($pieces) == 1) {
array_push($pieces, $default);
}
$gear = ucfirst(array_shift($pieces));
$cogear = getInstance();
if (isset($cogear->gears->$gear)) {
$gear_dir = $cogear->gears->$gear->dir;
$file_name = implode(DS, $pieces);
return $path = $gear_dir . DS . $dir . DS . $file_name;
} elseif ($found = find(ucfirst(str_replace('_', DS, $gear)) . DS . 'Gear' . EXT)) {
$gear_dir = dirname($found);
$file_name = implode(DS, $pieces);
return $path = $gear_dir . DS . $dir . DS . $file_name;
}
}
return NULL;
}
Запрос
Когда запрос поступает на шестеренку, вызывается этот метод, прежде чем дергать диспатчер./**
* Notify gear that it's requested by uri
*/
public function request() {
$this->is_requested = TRUE;
if(!event('gear.request',$this)){
return;
}
event('gear.request.' . strtolower($this->gear));
}
Диспатчер
Собственно тот метод, который обрабатывает запрос./**
* Dispatcher
* @param string $action
*/
public function index() {
$args = func_get_args();
method_exists($this, $args[0].'_action') && call_user_func_array(array($this,$args[0].'_action'),array_slice($args,1));
}
Если его нет — шестеренка через браузер отрабатываться не будет, за исключеним отдельных методов./gear/show/param1…/paramN = $Gear->index('show',$param1,…,$paramN)
// Если метод index отсутствует
/gear/show/param1…/paramN = $Gear->show_action($param1,…,$paramN)
Вот и все друзья. Ничего лишнего, только все самое необходимое. Это те возможности, которые предоставляет базовый класс. Вы же можете ограничиться тем набором, которым довольствуется шестеренка jQuery.
Как вам такие задумки?


Вот здесь — dev.cogear.ru/projects/cogear2/wiki/Слоеный_пирог актуальная инфа или нет? Что-то я куда в /sites/ шестеренку ни положу — она не видится.
Тогда вопрос — если меня все устраивает в целом в Pages, но не устраивает шаблон, смогу я перегрузить только его? Т.е. что-то типа
class MyPages_Gear extends Pages_Gear { protected $name = 'MyPages'; protected $description = 'MyPages contains only templates/page.php and Gear.php'; public function renderPage($page) { $tpl = new Template('MyPages.page'); $tpl->item = $page; append('content', $tpl->render()); } }Попробовал — не получается. Нужно перегружать всю шестеренку, со всеми потрохами?
Как их добавлять к сайту? Класть папочку в /engine/Themes/? Как-то не айс engine трогать. А если при апдейте движка юзер её снесет и запишет новую?
Я, в общем, вот что предлагаю. Сделать иерархическую файловую систему, как в Кохане. Т.е. если положить файлик в /sites/domain.com/gears/Pages/templates/page.php, то для отображения будет браться именно он, а не то, что в engine. И оформлять Pages как шестеренку в sites не надо. То же относится к папке css и ко всем другим папкам и файлам. Так для народа будет гораздо удобнее, мне кажется, и кастомабельность движка возрастет значительно. И темы можно будет класть в /sites/domain.com/gears/Themes/
Возможно, я чего-то не догоняю и в движке это уже есть, тогда сорри. :)