Вчера Вы научились интернационализировать и локализовать Ваши приложения на Symfony. Еще раз, благодаря стандартам ICU и множеству помощников, фреймворк Symfony делает это действительно простым.
Сегодня мы поговорим о плагинах: что они собой представляют, что Вы можете упаковать в плагин, и для чего они могут использоваться.
Плагины
Плагин Symfony
Плагин Symfony предлагает способ для упаковки и распространения подмножества файлов вашего проекта. Как и проект, плагин может содержать классы, помощники, конфигурацию, задачи, модули, схемы и даже стили (CSS) и картинки.
Приватные плагины
Для начала плагины используются для упрощения совместного использования в Ваших приложениях, или даже разных проектах. Вспомним, что приложения Symfony разделяют только модель. Плагины предоставляют способ для совместного использования большего количества компонентов в разных приложениях.
Если Вам нужно повторно использовать ту же схему для другого проекта, или те же модули, переместите
их в плагин. Поскольку плагин это просто папка, Вы можете подключать их достаточно просто с помощью
создания SVN-хранилища, используя svn:externals
, или просто копируя файлы из одного проекта
в другой.
Мы называем эти плагины "приватными" потому, что их использование ограничено одним разработчиком или компанией. Они не доступны публично.
tip
Вы можете даже создать пакет из Ваших приватных плагинов, создать собственный канал плагинов
Symfony и устанавливать их с помощью задачи plugin:install
.
Публичные плагины
Публичные плагины доступны для скачивания и установки всему сообществу. В течении
этого обучения мы использовали несколько публичных плагинов: sfDoctrineGuardPlugin
и sfFormExtraPlugin
.
Они точно такие же как и приватные плагины. Единственное отличие это то, что кто угодно может установить их для своих проектов. Позже Вы узнаете как публиковать и размещать публичные плагины на веб-сайте Symfony.
Различные способы организации кода
Есть еще один способ восприятия и использования плагинов. Забудьте о
повторном использовании и совместном использовании. Плагины могут быть использованы
для другого способа организации вашего кода. Вместо организации файлов по уровням:
все модели в папке lib/model/
, шаблоны в папке templates/
, ...; файлы группируются
по функциональности: все файлы, относящие к вакансиям, вместе (модель, модули и шаблоны),
все файлы CMS вместе, и так далее.
Структура файлов плагина
Плагин - это просто структура папок с файлами, организованная определенным образом,
в зависимости от характеристик файлов. Сегодня мы переместим большинство кода, который
мы написали для Jobeet в sfJobeetPlugin
. Простой макет который мы будем использовать:
sfJobeetPlugin/ config/ sfJobeetPluginConfiguration.class.php // Инициализация плагина routing.yml // Маршрутизация doctrine/ schema.yml // Схема базы данных lib/ Jobeet.class.php // Классы helper/ // Помощники filter/ // Классы фильтров form/ // Классы форм model/ // Классы модели task/ // Задачи modules/ job/ // Модули actions/ config/ templates/ web/ // JS, CSS и картинки
Плагин Jobeet
В начале просто создадим новую папку в plugins/
. Для Jobeet, давайте создадим папку sfJobeetPlugin
:
$ mkdir plugins/sfJobeetPlugin
Then, activate the sfJobeetPlugin
in config/ProjectConfiguration.class.php
file.
public function setup() { $this->enablePlugins(array( 'sfDoctrinePlugin', 'sfDoctrineGuardPlugin', 'sfFormExtraPlugin', 'sfJobeetPlugin' )); }
note
Названия всех плагинов должны заканчиваться на Plugin
. Также хорошая привычка ставить для них префикс
sf
, но это не обязательно.
Модель
Сначала переместите файл config/doctrine/schema.yml
в plugins/sfJobeetPlugin/config/
:
$ mkdir plugins/sfJobeetPlugin/config/ $ mkdir plugins/sfJobeetPlugin/config/doctrine $ mv config/doctrine/schema.yml plugins/sfJobeetPlugin/config/doctrine/schema.yml
note
Все командные строки приведены для Unix-подобного окружения. Если Вы используете Windows, Вы можете перетащить
файлы в эксплорере. И если Вы используете Subversion, или какой нибудь другой инструмент
для управления кодом, используйте предоставленные встроенные инструменты (например svn mv
для перемещения файлов).
Переместите файлы модели, форм, и фильтров в plugins/sfJobeetPlugin/lib/
:
$ mkdir plugins/sfJobeetPlugin/lib/ $ mv lib/model/ plugins/sfJobeetPlugin/lib/ $ mv lib/form/ plugins/sfJobeetPlugin/lib/ $ mv lib/filter/ plugins/sfJobeetPlugin/lib/ $ rm -rf plugins/sfJobeetPlugin/lib/model/doctrine/sfDoctrineGuardPlugin $ rm -rf plugins/sfJobeetPlugin/lib/form/doctrine/sfDoctrineGuardPlugin $ rm -rf plugins/sfJobeetPlugin/lib/filter/doctrine/sfDoctrineGuardPlugin
Remove the plugins/sfJobeetPlugin/lib/form/BaseForm.class.php
file.
$ rm plugins/sfJobeetPlugin/lib/form/BaseForm.class.php
После того, как Вы переместите модели, формы и фильтры, классы должны быть переименованы, сделаны
абстрактными и иметь префикс Plugin
.
tip
Добавляйте префикс Plugin
только автоматически сгенерированным классам, а не всем подряд.
Например, не надо добавлять префикс для классов, которые Вы написали в ручную.
Например, мы перемещаем классы JobeetAffiliate
и JobeetAffiliateTable
.
$ mv plugins/sfJobeetPlugin/lib/model/doctrine/JobeetAffiliate.class.php plugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetAffiliate.class.php
Код также должен быть обновлен:
abstract class PluginJobeetAffiliate extends BaseJobeetAffiliate { public function save(Doctrine_Connection $conn = null) { if (!$this->getToken()) { $this->setToken(sha1($object->getEmail().rand(11111, 99999))); } parent::save($conn); } // ... }
Теперь давайте переместим класс JobeetAffiliateTable
:
$ mv plugins/sfJobeetPlugin/lib/model/doctrine/JobeetAffiliateTable.class.php plugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetAffiliateTable.class.php
Определение класса должно выглядеть вот так:
abstract class PluginJobeetAffiliateTable extends Doctrine_Table { // ... }
Теперь мы сделаем тоже самое для классов форм и фильтров. Переименуйте их, вставляя
префикс Plugin
.
Удалите папку base
в plugins/sfJobeetPlugin/lib/*/doctrine/
в папках form
, filter
и model
:
$ rm -rf plugins/sfJobeetPlugin/lib/form/doctrine/base $ rm -rf plugins/sfJobeetPlugin/lib/filter/doctrine/base $ rm -rf plugins/sfJobeetPlugin/lib/model/doctrine/base
Как только Вы перенесете, переименуете и удалите некоторые классы форм, фильтров и моделей запустите задачи для построения всех классов:
$ php symfony doctrine:build-model $ php symfony doctrine:build-forms $ php symfony doctrine:build-filters
Теперь Вы заметите, что созданы некоторые новые папки для хранения моделей, созданных
из схемы, включенной в sfJobeetPlugin
из lib/model/doctrine/sfJobeetPlugin/
.
Эти папки содержат модели верхнего уровня и базовые классы, сгенерированные из схемы.
Например модель JobeetJob
теперь имеет такую структуру классов:
JobeetJob
(наследуетPluginJobeetJob
) вlib/model/doctrine/sfJobeetPlugin/JobeetJob.class.php
: Класс верхнего уровня, где может содержаться функциональность модели для всего проекта. Здесь Вы можете добавлять и перегружать функциональность, которая идет с моделями плагина.PluginJobeetJob
(наследуетBaseJobeetJob
) вplugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetJob.class.php
: Этот класс содержит всю функциональность, специфичную для плагина. Вы можете перегрузить функциональность этого и базового класса, изменяя классJobeetJob
.BaseJobeetJob
(наследуетsfDoctrineRecord
) вlib/model/doctrine/sfJobeetPlugin/base/BaseJobeetJob.class.php
: Базовый класс, который генерируется из yaml-файла схемы каждый раз, когда Вы запускаетеdoctrine:build-model
.JobeetJobTable
(наследуетPluginJobeetJobTable
) вlib/model/doctrine/sfJobeetPlugin/JobeetJobTable.class.php
: Такой же, как и классJobeetJob
, исключая то, что это экземплярDoctrine_Table
, который будет возвращен, когда Вы вызываетеDoctrine_Core::getTable('JobeetJob')
.PluginJobeetJobTable
(наследуетDoctrine_Table
) вlib/model/doctrine/sfJobeetPlugin/JobeetJobTable.class.php
: Этот класс содержит всю специфичную для плагина функциональность, для экземпляраDoctrine_Table
который будет возвращен, когда Вы вызываетеDoctrine_Core::getTable('JobeetJob')
.
Используя эту сгенерированную структуру, Вы можете настроить модели плагина,
отредактировав класс верхнего уровня JobeetJob
. Вы можете настроить схему
и добавить столбцы, добавить отношения с помощью перегрузки методов setTableDefinition()
и
setUp()
.
note
Когда Вы перемещаете классы форм, проверьте, что Вы изменили метод configure()
на метод
setup()
и вызываете parent::setup()
. Пример приведен ниже.
abstract class PluginJobeetAffiliateForm extends BaseJobeetAffiliateForm { public function setup() { parent::setup(); } // ... }
Мы должны убедиться, что в нашем плагине нет базовых классов для всех форм Doctrine.
Эти файлы глобальные для проекта и будут перегенерированы с помощью
doctrine:build-forms
и doctrine:build-filters
.
Удалите файлы из плагина:
$ rm plugins/sfJobeetPlugin/lib/form/doctrine/BaseFormDoctrine.class.php $ rm plugins/sfJobeetPlugin/lib/filter/doctrine/BaseFormFilterDoctrine.class.php
note
Если Вы используете Symfony 1.2.0 или 1.2.1, базовый класс фильтров форм находится в
папке plugins/sfJobeetPlugin/lib/filter/base/
.
Вы можете также перенести файл Jobeet.class.php
в плагин:
$ mv lib/Jobeet.class.php plugins/sfJobeetPlugin/lib/
Поскольку мы перемещали файлы, очистите кэш:
$ php symfony cc
tip
Если Вы используете ускоритель PHP такой как APC и происходит что-то странное на этом шаге, перезапустите Apache.
Теперь, когда все файлы модели перемещены в плагин, запустите тесты для того, чтобы проверить, что все работает:
$ php symfony test:all
Контроллеры и представления
Следующий логический шаг это переместить модули в плагин. Для предотвращения коллизий в названиях модулей, неплохо добавлять к названию модуля такой же префикс, как и название плагина:
$ mkdir plugins/sfJobeetPlugin/modules/ $ mv apps/frontend/modules/affiliate plugins/sfJobeetPlugin/modules/sfJobeetAffiliate $ mv apps/frontend/modules/api plugins/sfJobeetPlugin/modules/sfJobeetApi $ mv apps/frontend/modules/category plugins/sfJobeetPlugin/modules/sfJobeetCategory $ mv apps/frontend/modules/job plugins/sfJobeetPlugin/modules/sfJobeetJob $ mv apps/frontend/modules/language plugins/sfJobeetPlugin/modules/sfJobeetLanguage
Для каждого модуля, Вы также должны изменить названия классов во всех файлах
actions.class.php
и components.class.php
(например, класс
affiliateActions
нужно переименовать в sfJobeetAffiliateActions
).
Вызовы include_partial()
и include_component()
должны быть так же изменены в следующих
шаблонах:
sfJobeetAffiliate/templates/_form.php
(изменитеaffiliate
наsfJobeetAffiliate
)sfJobeetCategory/templates/showSuccess.atom.php
sfJobeetCategory/templates/showSuccess.php
sfJobeetJob/templates/indexSuccess.atom.php
sfJobeetJob/templates/indexSuccess.php
sfJobeetJob/templates/searchSuccess.php
sfJobeetJob/templates/showSuccess.php
apps/frontend/templates/layout.php
Обновите действия search
и delete
:
// plugins/sfJobeetPlugin/modules/sfJobeetJob/actions/actions.class.php class sfJobeetJobActions extends sfActions { public function executeSearch(sfWebRequest $request) { $this->forwardUnless($query = $request->getParameter('query'), 'sfJobeetJob', 'index'); $this->jobs = Doctrine_Core::getTable('JobeetJob') ->getForLuceneQuery($query); if ($request->isXmlHttpRequest()) { if ('*' == $query || !$this->jobs) { return $this->renderText('No results.'); } return $this->renderPartial('sfJobeetJob/list', array('jobs' => $this->jobs)); } } public function executeDelete(sfWebRequest $request) { $request->checkCSRFProtection(); $jobeet_job = $this->getRoute()->getObject(); $jobeet_job->delete(); $this->redirect('sfJobeetJob/index'); } // ... }
Теперь обновите файл routing.yml
для того, чтобы применить эти изменения:
# apps/frontend/config/routing.yml affiliate: class: sfDoctrineRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: GET } prefix_path: /:sf_culture/affiliate module: sfJobeetAffiliate requirements: sf_culture: (?:fr|en) api_jobs: url: /api/:token/jobs.:sf_format class: sfDoctrineRoute param: { module: sfJobeetApi, action: list } options: { model: JobeetJob, type: list, method: getForToken } requirements: sf_format: (?:xml|json|yaml) category: url: /:sf_culture/category/:slug.:sf_format class: sfDoctrineRoute param: { module: sfJobeetCategory, action: show, sf_format: html } options: { model: JobeetCategory, type: object, method: doSelectForSlug } requirements: sf_format: (?:html|atom) sf_culture: (?:fr|en) job_search: url: /:sf_culture/search param: { module: sfJobeetJob, action: search } requirements: sf_culture: (?:fr|en) job: class: sfDoctrineRouteCollection options: model: JobeetJob column: token object_actions: { publish: PUT, extend: PUT } prefix_path: /:sf_culture/job module: sfJobeetJob requirements: token: \w+ sf_culture: (?:fr|en) job_show_user: url: /:sf_culture/job/:company_slug/:location_slug/:id/:position_slug class: sfDoctrineRoute options: model: JobeetJob type: object method_for_query: retrieveActiveJob param: { module: sfJobeetJob, action: show } requirements: id: \d+ sf_method: GET sf_culture: (?:fr|en) change_language: url: /change_language param: { module: sfJobeetLanguage, action: changeLanguage } localized_homepage: url: /:sf_culture/ param: { module: sfJobeetJob, action: index } requirements: sf_culture: (?:fr|en) homepage: url: / param: { module: sfJobeetJob, action: index } Если Вы сейчас попробуете просмотреть веб-сайт Jobeet, Вы увидите исключения,
которые говорят о том, что модули не включены. Поскольку плагины являются общими для всех
приложений в проекте, Вам нужно специально включить модули которые Вам нужны для данного
приложения в его файле настроек settings.yml
:
# apps/frontend/config/settings.yml all: .settings: enabled_modules: - default - sfJobeetAffiliate - sfJobeetApi - sfJobeetCategory - sfJobeetJob - sfJobeetLanguage
Последний шаг миграции - это исправление функциональных тестов, где мы тестируем название модуля.
Задачи
Задачи могут быть перемещены в плагин достаточно легко:
$ mv lib/task plugins/sfJobeetPlugin/lib/
Файлы i18n
Плагин может также содержать файлы XLIFF:
$ mv apps/frontend/i18n plugins/sfJobeetPlugin/
Маршрутизация
Плагин может также содержать правила маршрутизации:
$ mv apps/frontend/config/routing.yml plugins/sfJobeetPlugin/config/
Веб-содержимое
Даже если это немного нелогично, плагин может также содержать собственные картинки, стили (CSS) и
JavaScripts. Поскольку мы не хотим распространять плагин Jobeet, в действительности это не имеет
смысла, но это возможно с помощью создания папки plugins/sfJobeetPlugin/web/
.
Веб-содержимое плагина должно быть доступно в папке web/
проекта для того, чтобы его можно
было просмотреть из браузера. Задача plugin:publish-assets
заботится об этом, создавая
символические ссылки в Unix системах и копируя файлы на платформе Windows:
$ php symfony plugin:publish-assets
Пользователь
Перемещение методов класса myUser
, которые имеют дело с историей вакансий чуть сложнее.
Мы можем создать класс JobeetUser
и сделать так, чтобы класс myUser
наследовался от него.
Но есть решение получше, особенно если несколько плагинов хотят добавить методы в класс.
Объекты ядра Symfony оповещают о событиях в течение их жизненного цикла, и Вы можете их
слушать. В нашем случае нужно слушать событие user.method_not_found
, которое происходит когда
вызывается метод не определенный в объекте sfUser
.
Когда Symfony инициализирован, все плагины тоже инициализированы, если у них есть класс конфигурации плагина:
// plugins/sfJobeetPlugin/config/sfJobeetPluginConfiguration.class.php class sfJobeetPluginConfiguration extends sfPluginConfiguration { public function initialize() { $this->dispatcher->connect('user.method_not_found', array('JobeetUser', 'methodNotFound')); } }
Оповещение о событиях управляется с помощью
sfEventDispatcher
,
объекта диспечера событий. Регистрация слушателя настолько же проста, как и вызов
метода connect()
. Метод connect()
подключает название события к вызываемым PHP-объектам.
note
Вызываемый PHP-объект это
переменная PHP которая может быть использована функцией call_user_func()
и возвращает
true
когда будет передана в функцию is_callable()
. Строка представляет функцию,
а массив может представлять метод объекта или метод класса.
С помощью следующего кода объект myUser
будет вызывать статический метод
methodNotFound()
класса JobeetUser
каждый раз, когда не сможет найти метод.
После этого все зависит от метода methodNotFound()
- обрабатывать пропущенный метод или нет.
Удалите все методы из класса myUser
и создайте класс JobeetUser
:
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { } // plugins/sfJobeetPlugin/lib/JobeetUser.class.php class JobeetUser { static public function methodNotFound(sfEvent $event) { if (method_exists('JobeetUser', $event['method'])) { $event->setReturnValue(call_user_func_array( array('JobeetUser', $event['method']), array_merge(array($event->getSubject()), $event['arguments']) )); return true; } } static public function isFirstRequest(sfUser $user, $boolean = null) { if (is_null($boolean)) { return $user->getAttribute('first_request', true); } else { $user->setAttribute('first_request', $boolean); } } static public function addJobToHistory(sfUser $user, JobeetJob $job) { $ids = $user->getAttribute('job_history', array()); if (!in_array($job->getId(), $ids)) { array_unshift($ids, $job->getId()); $user->setAttribute('job_history', array_slice($ids, 0, 3)); } } static public function getJobHistory(sfUser $user) { $ids = $user->getAttribute('job_history', array()); if (!empty($ids)) { return Doctrine_Core::getTable('JobeetJob') ->createQuery('a') ->whereIn('a.id', $ids) ->execute(); } return array(); } static public function resetJobHistory(sfUser $user) { $user->getAttributeHolder()->remove('job_history'); } }
Когда диспечер вызовет метод methodNotFound()
, он передаст объект
sfEvent
.
Если метод существует в классе JobeetUser
, он будет вызван, и его возвращаемое значение
будет передано дальше к регистратору. Если нет, Symfony будет пробовать найти
следующий слушатель или вернет исключение.
Метод getSubject()
возвращает регистратора события, в данном случае это объект myUser
.
Структура по умолчанию против архитектуры плагина
Использование архитектуры плагина дает Вам возможность организовать Ваш код другим способом:
Использование плагинов
Когда Вы начинаете внедрять новую функциональность, или если Вы пытаетесь решить классическую проблему веб-программирования, возможно кто-то другой уже решил такую же проблему и возможно упаковал решение как плагин Symfony. Посмотрите в публичных плагинах Symfony в секции плагинов на сайте фреймворка Symfony.
Поскольку плагин - это самодостаточная папка, есть несколько способов для его установки:
- Используя задачу
plugin:install
(работает только если разработчик плагина создал пакет плагина и загрузил его на веб-сайт Symfony) - Загрузите пакет и вручную распакуйте его в папку
plugins/
(также нужно, чтобы разработчик загрузил пакет) - Создание
svn:externals
вplugins/
для плагина (работает только если разработчик плагина хостит этот плагин в Subversion)
Последние два способа простые, но им не хватает гибкости. Первый способ позволяет Вам установить последнюю версию в зависимости от версии Symfony в проекте, легко обновить до последнего стабильного релиза, и легче управлять зависимостями между плагинами.
Создание плагина
Упаковка плагина
Для создания пакета плагина, нужно добавить некоторые обязательные файлы в
структуру папок плагина. Сначала создайте файл README
в корневой папке плагина
и опишите как установить плагин, что он предоставляет и что нет.
Файл README
должен быть отформатирован в
формате Markdown. Этот файл
будет использован веб-сайтом Symfony как основная часть документации.
Вы можете протестировать трансформацию вашего файла README в HTML используя
Symfony plugin dingus.
Вам также нужно создать файл LICENSE
. Выбор лицензии не простая задача,
но в секции плагинов Symfony показываются только плагины
выпущенные под лицензией подобной лицензии Symfony (MIT, BSD, LGPL, and PHP).
Содержимое файла LICENSE
будет отображено на вкладке лицензии публичной страницы
вашего плагина.
Последний шаг это создание файла package.xml
в корневой папке плагина.
Этот файл package.xml
соответствует
Синтаксис пакета PEAR.
note
Лучший способ выучить синтаксис package.xml
- это скопировать его из
существующего плагина.
Как Вы видите в этом примере, файл package.xml
состоит из нескольких частей:
<!-- plugins/sfJobeetPlugin/package.xml --> <?xml version="1.0" encoding="UTF-8"?> <package packagerversion="1.4.1" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd" > <name>sfJobeetPlugin</name> <channel>plugins.symfony-project.org</channel> <summary>A job board plugin.</summary> <description>A job board plugin.</description> <lead> <name>Fabien POTENCIER</name> <user>fabpot</user> <email>fabien.potencier@symfony-project.com</email> <active>yes</active> </lead> <date>2008-12-20</date> <version> <release>1.0.0</release> <api>1.0.0</api> </version> <stability> <release>stable</release> <api>stable</api> </stability> <license uri="http://www.symfony-project.com/license"> MIT license </license> <notes /> <contents> <!-- CONTENT --> </contents> <dependencies> <!-- DEPENDENCIES --> </dependencies> <phprelease> </phprelease> <changelog> <!-- CHANGELOG --> </changelog> </package>
Тэг <contents>
содержит файлы которые нужно поместить в пакет:
<contents> <dir name="/"> <file role="data" name="README" /> <file role="data" name="LICENSE" /> <dir name="config"> <file role="data" name="config.php" /> <file role="data" name="schema.yml" /> </dir> <!-- ... --> </dir> </contents>
Тэг <dependencies>
указывает все зависимости которые могут быть у плагина:
PHP, symfony, и также другие плагины. Эта информация используется задачей
plugin:install
для установки самой подходящей версии плагина для окружения
проекта, и так же устанавливает плагины, требуемые в зависимостях если они есть.
<dependencies> <required> <php> <min>5.0.0</min> </php> <pearinstaller> <min>1.4.1</min> </pearinstaller> <package> <name>symfony</name> <channel>pear.symfony-project.com</channel> <min>1.3.0</min> <max>1.5.0</max> <exclude>1.5.0</exclude> </package> </required> </dependencies>
Вы должны всегда указывать зависимость от Symfony, как мы сделали тут.
Заявление минимальной и максимальной версии дает возможность задаче
plugin:install
узнать, какая версия Symfony обязательна, поскольку разные
версии фреймворка имеют очень разные API.
Так же возможно заявление зависимостей от других плагинов:
<package> <name>sfFooPlugin</name> <channel>plugins.symfony-project.org</channel> <min>1.0.0</min> <max>1.2.0</max> <exclude>1.2.0</exclude> </package>
Тэг <changelog>
опциональный, но дает полезную информацию о том, что было
изменено между релизами. Эта информация так же доступна на вкладке "Changelog"
и в ленте плагинов.
<changelog> <release> <version> <release>1.0.0</release> <api>1.0.0</api> </version> <stability> <release>stable</release> <api>stable</api> </stability> <license uri="http://www.symfony-project.com/license"> MIT license </license> <date>2008-12-20</date> <license>MIT</license> <notes> * fabien: First release of the plugin </notes> </release> </changelog>
Размещение плагинов на веб-сайте Symfony
Если Вы разработали полезный плагин и хотите поделиться им с сообществом Symfony, создайте аккаунт если у Вас его еще нет и затем создайте новый плагин.
Вы автоматически станете администратором для плагина и увидите вкладку "admin" в интерфейсе. В этой вкладке Вы найдете все, что нужно для управления Вашим плагином и загрузки Ваших пакетов.
note
FAQ по плагинам содержит много полезной информации для разработчиков плагинов.
Увидимся завтра!
Создание плагинов и совместное их использование в сообществе - это один из лучших способов внести вклад в проект Symfony. Это настолько просто, что хранилище плагинов заполнено полезными, забавными, а иногда и нелепыми плагинами.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.