Powered by CodeIgniter

Уроки

(28)
14
16 голосов
Учиться, учиться и еще раз учиться — развитие личности идет таким путем.
Доброго утра и хорошего всем дня! Вчера, читая Хабр, наткнулся на статью про создание модуля для Magento. Это известная в мире система от наших украинских коллег для создания Интернет-магазина. Читая статью, меня одолевал один единственный вопрос — зачем так усложнять? Такой же вопрос я постоянно задаю себе в сложные моменты разработки, разворачивая наш корабль на курсы простоты и эффективности. Помните, что простота еще не означает «говнокода» или непрофессионализма. Как говорится, все гениальное просто. Давайте посмотрим, что требуется для того, чтобы создать шестеренку для cogear².
Существование шестеренки определяется папкой и расположенном в ней классе шестеренки. Все. Никаких XML-файлов и других шалостей.
Обратимся к шестеренке 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.

Как вам такие задумки?
12:08 ← 27 июля 2011 Отправить в Твиттер adminadmin  RSS comments 8

Комментарии (8) ↓

JiLiZART JiLiZART time 13:35 ← 27 июля 2011 #
А потом берём class jQueryUI extends jQuery {} :)
Автор
admin admin time 14:07 ← 27 июля 2011 #
А вот в темах чуть иначе. Там при наследовании одной темы от другой, скрипты и стили родительской темы все равно подгружаются :-)
medar medar time 12:24 ← 04 августа 2011 #
А где должны лежать шестеренки, специфичные для домена? Например, чтобы перегрузить Pages, так как там задается роут на индекс. Я думал, что в /sites/domain.com/gears, но, похоже, это не так. И вообще, как сайт писать? Не делать же шестеренку Mysite в /engine?

Вот здесь — dev.cogear.ru/projects/cogear2/wiki/Слоеный_пирог актуальная инфа или нет? Что-то я куда в /sites/ шестеренку ни положу — она не видится.
Автор
admin admin time 13:00 ← 04 августа 2011 #
medar medar time 13:37 ← 04 августа 2011 #
Ага, заработало.
Тогда вопрос — если меня все устраивает в целом в 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()); } }
Попробовал — не получается. Нужно перегружать всю шестеренку, со всеми потрохами?
Автор
admin admin time 16:21 ← 04 августа 2011 #
Пока да. Но это пару минут сделать — внести такие изменения.
medar medar time 14:51 ← 04 августа 2011 #
И вдогонку про темы. Где менять название темы, которая юзается для сайта?
Как их добавлять к сайту? Класть папочку в /engine/Themes/? Как-то не айс engine трогать. А если при апдейте движка юзер её снесет и запишет новую?

Я, в общем, вот что предлагаю. Сделать иерархическую файловую систему, как в Кохане. Т.е. если положить файлик в /sites/domain.com/gears/Pages/templates/page.php, то для отображения будет браться именно он, а не то, что в engine. И оформлять Pages как шестеренку в sites не надо. То же относится к папке css и ко всем другим папкам и файлам. Так для народа будет гораздо удобнее, мне кажется, и кастомабельность движка возрастет значительно. И темы можно будет класть в /sites/domain.com/gears/Themes/

Возможно, я чего-то не догоняю и в движке это уже есть, тогда сорри. :)
Автор
admin admin time 16:21 ← 04 августа 2011 #
Сделаю.