четверг, 25 декабря 2008 г.

Символ 2009 года - в подарок!

С Новым годом и Рождеством, дорогие друзья!

Желаю всем психического здоровья!
В качестве объекта для успокоения нервов и предпраздничного мидитирования предлагаю символ года в виде бумажной коровы (скачать).




Энжой! :)

пятница, 14 ноября 2008 г.

Symfony 1.2: file upload in admin

Это черновик

/backend/modules/author/lib/form/myAuthorForm.class.php

/**
* Author form.
*
* @package any
* @subpackage form
* @author Your name here
* @version SVN: $Id: sfPropelFormTemplate.php 10377 2008-07-21 07:10:32Z dwhittle $
*/
class myAuthorForm extends BaseAuthorForm
{
public function configure()
{
$this->setWidget('description', new myWidgetFormRichTextarea(array('editor'=>'fck')));
$values = $this->getValues();
$this->setWidget('pic', new myWidgetFormInputFileEditable(
array(
'is_image' => true,
'delete_label' => 'удалить'
)));
//$this->setWidget('pic', new sfWidgetFormInputFile());
$this->setWidget('avatar', new sfWidgetFormInputFile());
/* VALIDATORS */
$this->setValidators(array(
'f_name' => new sfValidatorString(
array(),
array('required' => 'Обязательное поле')
),
'l_name' => new sfValidatorString(
array(),
array('required' => 'Обязательное поле')
),
'email' => new sfValidatorEmail(
$options = array(),
$messages = array(
'invalid' => 'Введите правильный Email',
'required' => false)
),
'pic' => new sfValidatorFile(array(
'required' => false,
'max_size' => '102400', // bytes (1MB)
'mime_types' => array('image/png', 'image/jpeg',)
)),
));

//$this->widgetSchema->setNameFormat('%s');

$this->validatorSchema->setOption('allow_extra_fields', true);
$this->validatorSchema->setOption('filter_extra_fields', false);
}//configure

public function updateObject($values = null)
{
$object = parent::updateObject();
$path = sfConfig::get('sf_root_dir').DIRECTORY_SEPARATOR.
sfConfig::get('sf_web_dir_name').DIRECTORY_SEPARATOR.sfConfig::get('sf_upload_dir');
$object->setPic(str_replace($path.'/', '', $object->getPic()));
return $object;
}
/* do save*/
protected function doSave($con = null){
$values = $this->getValues();

$path = sfConfig::get('sf_root_dir').'/'.
sfConfig::get('sf_web_dir_name').'/'.sfConfig::get('sf_upload_dir');

if (isset($values['pic_delete'])){
$currentFile = $path.'/'.$this->getObject()->getPic();
if (is_file($currentFile)){
unlink($currentFile);
}
$this->getObject()->setPic('');
}

$file = $values['pic'];

if(!empty($file)){
if (file_exists(sfConfig::get('sf_upload_dir').'/'.$this->getObject()->getPic())){
@unlink($this->getObject()->getPic());
}
$filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
$path = sfConfig::get('sf_root_dir').DIRECTORY_SEPARATOR.sfConfig::get('sf_web_dir_name').DIRECTORY_SEPARATOR.sfConfig::get('sf_upload_dir');
$file->save($path.'/'.$filename);
}

return parent::doSave($con);
}
}//class

/lib/myWidgetFormInputFileEditable.class.php

/*
* This file is part of the symfony package.
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/**
* sfWidgetFormInputFileEditable represents an upload HTML input tag with the possibility
* to remove a previously uploaded file.
*
* @package symfony
* @subpackage widget
* @author Fabien Potencier
* @version SVN: $Id: sfWidgetFormInputFileEditable.class.php 11544 2008-09-14 17:40:07Z fabien $
*/
class myWidgetFormInputFileEditable extends sfWidgetFormInputFile
{
/**
* Constructor.
*
* Available options:
*
* * file_src: The current image web source path (required)
* * edit_mode: A Boolean: true to enabled edit mode, false otherwise
* * is_image: Whether the file is a displayable image
* * with_delete: Whether to add a delete checkbox or not
* * delete_label: The delete label used by the template
* * template: The HTML template to use to render this widget
* The available placeholders are:
* * input (the image upload widget)
* * delete (the delete checkbox)
* * delete_label (the delete label text)
* * file (the file tag)
*
* In edit mode, this widget renders an additional widget named after the
* file upload widget with a "_delete" suffix. So, when creating a form,
* don't forget to add a validator for this additional field.
*
* @param array $options An array of options
* @param array $attributes An array of default HTML attributes
*
* @see sfWidgetFormInputFile
*/
protected function configure($options = array(), $attributes = array())
{
parent::configure($options, $attributes);

$this->setOption('type', 'file');
$this->setOption('needs_multipart', true);

//$this->addRequiredOption('file_src');
$this->addOption('file_src', '');
$this->addOption('is_image', false);
$this->addOption('edit_mode', true);
$this->addOption('with_delete', true);
$this->addOption('delete_label', 'remove the current file');
$this->addOption('template', '%file% %input% %delete% %delete_label%');
}

/**
* @param string $name The element name
* @param string $value The value displayed in this widget
* @param array $attributes An array of HTML attributes to be merged with the default HTML attributes
* @param array $errors An array of errors for the field
*
* @return string An HTML tag string
*
* @see sfWidgetForm
*/
public function render($name, $value = null, $attributes = array(), $errors = array())
{

$input = parent::render($name, $value, $attributes, $errors);

if (!$this->getOption('edit_mode'))
{
return $input;
}

if ($this->getOption('with_delete')){
$deleteName = ']' == substr($name, -1) ? substr($name, 0, -1).'_delete]' : $name.'_delete';
$delete = $this->renderTag('input', array_merge(array('type' => 'checkbox', 'name' => $deleteName), $attributes));
$deleteLabel = $this->renderContentTag('span', $this->getOption('delete_label'), array_merge(array('for' => $this->generateId($deleteName))));
} else {
$delete = '';
$deleteLabel = '';
}
if ($value){
$picLink = " || <a class='thickbox' target='_blank' href='/uploads/".$value."'>show me</a>";
} else {
$picLink = "";
$delete = '';
$deleteLabel = '';
}

return strtr($this->getOption('template'), array(
'%input%' => $input,
'%delete%' => $delete,
'%delete_label%' => $deleteLabel,
'%file%' => $this->getFileAsTag($attributes, $value))
).$picLink;
}

protected function getFileAsTag($attributes, $value=null)
{
if ($this->getOption('is_image'))
{
return false !== $value ? $this->renderTag('img', array_merge(array('src' => $value)), $attributes) : '';
}
else
{
return $this->getOption('file_src');
}
}
}

/backend/config/view.yml
default:
http_metas:
content-type: text/html
metas:
title: Admin
stylesheets: [main, thickbox]
javascripts: [jq/jquery.pack.js, jq/thickbox.pack.js]

Symfony: sfGuard custom login form

Немного улучшим дизайн формы ввода логина.


myGuard.css
.error_list { color: #ff0000; }
.errorField{
background: #ffffcc; border: 2px solid red;
}
.okField{
background: #ffffcc; border: 2px solid lightgreen;
}
.inputField{
background: #ffffff;
}

#loginForm {
display: none;
background: khaki; width: 280px;
position: absolute;
z-index: 9999; font-family: Tahoma; font-size: 12px;
border: 0px solid red;
}

#loginFormHeader {
background: #cccc33; padding: 10px;
border-bottom: 1px solid gray;
font-weight: bold;
}
#loginFormBody {
border-top: 1px solid #ececec;
border-bottom: 1px solid gray;
padding: 4px 0px 4px 0px;
}

#loginFormPic {
padding: 10px 0px 0px 10px;
display: block;
float: left;
}
#loginFormBody ul{
float: left;
display: block;
border: 0px solid navy;
padding: 4px; list-style: none;
}

#loginFormBody ul li{
border: 0px solid #f63;
padding: 4px;
}

#loginFormBody label
{
display: block;
border: 0px solid green;
padding: 0 1em 3px 0;
float: left;
text-align: left;
width: 40px;
color: black;
font-weight: normal !important;
}
#loginFormFooter {
padding: 10px;
text-align: center;
border-top: 1px solid #ececec;
}


/backend/config/view.yml
signinSuccess:
javascripts: [jq/jquery.pack.js, jq/jquery.dropshadow.js]
stylesheets: [main, myGuard]
metas:
title: login

/backend/modules/sfGuardAuth/signinSuccess.php
<?php $errorFieldClass = "errorField" ?>
<?php $defaultFieldClass = ($sf_request->isMethod('post') ? "okField" : "inputField" )?>

<form action="<?php echo url_for('@sf_guard_signin') ?>"
method="post">
<div id="loginForm">
<div id="loginFormHeader">Требуется авторизация</div>
<div id="loginFormBody">
<div id="loginFormPic"><img src="/sf/sf_default/images/icons/lock48.png" /></div>
<ul>
<li><label for="signin_username">Логин:</label>
<?php echo $form['username']->render(
array(
'class' => $form['username']->hasError() ? $errorFieldClass : $defaultFieldClass))
?>
</li>
<li><label for="signin_password">Пароль:</label>
<?php echo $form['password']->render(
array(
'class' => $form['password']->hasError() ? $errorFieldClass : $defaultFieldClass))
?>
</li>
<li><label for="signin_remember"></label>
<?php echo $form['remember']->render() ?> Запомнить?
</li>
</ul>
<br clear="all" />
</div>
<div id="loginFormFooter">
<input type="submit" value="войти" />
</div>
</div>
</form>
<script>
$(document).ready(function() {
$("#loginForm").hide();
var popupX = Math.round( ($(window).width() - $("#loginForm").width()) / 2) ;
var popupY = $(document).scrollTop() + Math.round($(window).height()/2) - Math.round($("#loginForm").height()/2);
$("#loginForm").css({top: popupY+"px", left: popupX+"px"});
$("#signin_username").focus();
$("#loginForm").show();
$("#loginForm").dropShadow();//{left: -2, top: -2, blur: 4, color: "#03f"}
});
</script>

вторник, 11 ноября 2008 г.

Symfony 1.2: вышло обновление - beta2

Очень оперативно работают парни, 10 дней прошло после выхода beta1, и вот вам обновление.
Список "фиксов" тут:
http://trac.symfony-project.org/query?status=closed&milestone=1.2.0+BETA2

Теперь о неприятном ;)
После обновления я сразу получил ошибку
500 | Internal Server Error | sfConfigurationException
The route "articles_collection" does not exist.


Видимо опять что то изменилось в системе роутинга, беглый просмотр офиц.сайта не дал ответа что далать, пришлось "методом научного тыка" сделать костыль.
articles_collection:
url: /article/:action/*

articles:
class: sfPropelRouteCollection
options:
model: Article
module: article
with_show: false
collection_actions: { filter: post, batch: post }

Не знаю, прав ли я???

среда, 5 ноября 2008 г.

Symfony 1.2: подводные камни

1. В схеме нельзя давать имя модели Comment, это имя теперь зарезервировано Propel 1.3, используйте BlogComment и т.п.

2. Новый админ-генератор использует routing.yml, перед генерацией админки надо обязательно вписать в него блок такого типа:
articles:
class: sfPropelRouteCollection
options:
model: BlogArticle
module: article
collection_actions: { filter: post, batch: post }

Внимание!!!
Блок вставлять в начало routing.yml иначе получите ошибку
404 | Not Found | sfError404Exception
Action "articles/index" does not exist.

3. Помимо генерации модели и форм командами propel:generate-model, propel:generate-forms, теперь еще надо генерировать и фильтры(при первом взгляде те же формы) командой propel:generate-filters. Либо просто propel:generate-all.

...to be continued...

воскресенье, 2 ноября 2008 г.

среда, 29 октября 2008 г.

Symfony admin: изменяем стиль постраничной навигации

Лёгкий тюнинг CSS
.float-right span{
border: 1px solid #ff9900;
padding: 4px;
background: #eeeeff;
}
в /backend/templates/layout.php дает следующий результат

Symfony custom widget to display nested tree in listbox

Для формирования древовидной структуры достаточно ввести название для ветки и указать родительскую ветку.


Требования:
- Symfony 1.1.x
- Propel 1.3

/lib/myWidgetFormPropelSelectTree.class.php

<?php

/*
* widget displays nested tree in listbox
* based on symfony 1.1 and Propel 1.3
* 28-okt-2008 by Vit <228vit@gmail.com>
*/
class myWidgetFormPropelSelectTree extends sfWidgetFormSelect
{
/**
* @see sfWidget
*/
public function __construct($options = array(), $attributes = array())
{
$options['choices'] = new sfCallable(array($this, 'getChoices'));

parent::__construct($options, $attributes);
}

/**
* Constructor.
*
* Available options:
*
* * model: The model class (required)
* * add_empty: Whether to add a first empty value or not (false by default)
* If the option is not a Boolean, the value will be used as the text value
* * method: The method to use to display object values (__toString by default)
* * order_by: An array composed of two fields:
* * The column to order by the results (must be in the PhpName format)
* * asc or desc
* * criteria: A criteria to use when retrieving objects
* * connection: The Propel connection to use (null by default)
* * multiple: true if the select tag must allow multiple selections
*
* @see sfWidgetFormSelect
*/
protected function configure($options = array(), $attributes = array())
{
$this->addRequiredOption('model');
$this->addOption('add_empty', false);
$this->addOption('method', '__toString');
$this->addOption('order_by', null);//no needed, tree must be sorted
$this->addOption('criteria', null);
$this->addOption('connection', null);
$this->addOption('multiple', false);

parent::configure($options, $attributes);
}

/**
* Returns the choices associated to the model.
*
* @return array An array of choices
*/
public function getChoices()
{
$choices = array();
if (false !== $this->getOption('add_empty'))
{
$choices[''] = true === $this->getOption('add_empty') ? '' : $this->getOption('add_empty');
}

$class = $this->getOption('model').'Peer';
$criteria = is_null($this->getOption('criteria')) ? new Criteria() : clone $this->getOption('criteria');

$method = $this->getOption('method');

if (!method_exists($this->getOption('model'), $method))
{
throw new RuntimeException(sprintf('Class "%s" must implement a "%s" method to be rendered in a "%s" widget', $this->getOption('model'), $method, __CLASS__));
}
//get all SCOPEs from a tree
$cr = new Criteria;
$method2 = 'addAscendingOrderByColumn';
$cr->addAscendingOrderByColumn(
call_user_func(array($class, 'translateFieldName'),
'Scope',
BasePeer::TYPE_PHPNAME, BasePeer::TYPE_COLNAME)
);
$cr->addGroupByColumn(
call_user_func(array($class, 'translateFieldName'),
'Scope',
BasePeer::TYPE_PHPNAME, BasePeer::TYPE_COLNAME)
);
$scopes = call_user_func(array($class, 'doSelect'), $cr, $this->getOption('connection'));
$scopesCnt = count($scopes)-1;
//loop over scopes
foreach ($scopes as $currScope => $scope){
//retrieve tree for current scope
//TreePeer::retrieveTree($scope->getScope());
$tree = call_user_func(array($class, 'retrieveTree'), $scope->getScope(), $this->getOption('connection'));
//start iterator
$it = new myTreeListboxOptions($tree);
//loop over childs
foreach ($it as $m){
//store attributes in arrays, to build row for listbox
$siblings[$m->getLevel()] = intval($m->hasNextSibling());
$childs[$m->getLevel()] = intval($m->hasChildren());
$currLevel = $m->getLevel();
$connectors = "";//extra string which contains connectors:


В форме описываем как:

$this->setWidgets(array(
'id' => new sfWidgetFormInputHidden(),
'name' => new sfWidgetFormInput(),
...
'parent' => new myWidgetFormPropelSelectTree(
array(
'model' => 'Menu',
'add_empty' => 'root')),
));

$this->widgetSchema->setNameFormat('%s');
$this->validatorSchema->setOption('allow_extra_fields', true);
$this->validatorSchema->setOption('filter_extra_fields', false);


В пример для обработки в действии:

   if ($this->form->isValid()){
$tree = new Menu();
$tree->setName($request->getParameter('name'));
$parent = $request->getParameter('parent');

//get max scope
$nextScope = 0;
$c = new Criteria;
$c->clearSelectColumns()->addSelectColumn('MAX(' . MenuPeer::SCOPE . ')');
$stmt = MenuPeer::doSelectStmt($c);
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
$nextScope = $row[0] + 1;
}
$nextScope = (is_null($nextScope) ? 1 : $nextScope);

if (empty($parent)){
$tree->makeRoot();
$tree->setScopeIdValue($nextScope);
} else {
$root = MenuPeer::retrieveByPK($parent);
$tree->insertAsLastChildOf($root);
}
$tree->save();
}//if valid

понедельник, 20 октября 2008 г.

jQuery: custom popup on center screen

При обработке событий джаваскриптом, часто надо показывать слой с результатом работы и ходом исполнения по центру экрана. Проблемы вычисления центра возникают если экран имеет вертикальную прокрутку, вот решение:

$(document).ready(function() {

var popupX = Math.round( ($(window).width() - $("#result_div").width()) / 2) ;
var popupY = $(document).scrollTop() + Math.round($(window).height()/2) - Math.round($("#result_div").height()/2);
$("#result_div").css({top: popupY+"px", left: popupX+"px"});
$("#result_div").slideDown("slow");
...

пятница, 17 октября 2008 г.

Алфавит циклом, в кодировке UTF-8

Выбор по алфавиту:

<?php for($i = 144; $i <= 175; $i++): ?>
<a href="getModuleName()."/list?letter=".chr($i)) ?>">
<?php echo chr(208).chr($i)?></a>
<?php endfor; ?>


Получаем:
А Б В Г Д Е Ж З И Й ...

Буква Ё отсутствует, восстановим справедливость:

<?php for($i = 144; $i <= 175; $i++): ?>
<a href="<?php echo url_for($this->getModuleName()."/list?letter=".chr(208).chr($i)) ?>">
<?php echo chr(208).chr($i)?></a>
<?php if ($i == 149): ?>
<a href="<?php echo url_for($this->getModuleName()."/list?letter=".chr(208).chr(129)) ?>">
<?php echo chr(208).chr(129)?></a>
<?php endif; ?>
<?php endfor; ?>

Propel 1.3: маленькая проблема при работе с nested sets

Если вы вдруг в процессе разработки проекта решили, что самое время перейти на Propel 1.3, для использования его встроенной поддержки древовидных структур(nested sets), не наступите на мои "грабли", а именно:
после установки Propel 1.3, внесения изменений в таблицу и регенерации модели, убедитесь, что ваша старая модель(в моем случае /lib/model/Categorie.php) стала наследовать новую, пример:

//было
class Categorie extends BaseCategorie
//стало
class Categorie extends BaseCategorieNestedSet

Дело в том, что генератор Propel увидев, что я вносил изменения в /lib/model/Categorie.php не стал его трогать, и менять extends BaseCategorie, в результате я получил ошибки, при попытке использовать методы типа:
$root->makeRoot();
$menu->insertAsLastChildOf($root);
Доверяй генератору, но проверяй ;)

среда, 15 октября 2008 г.

Symfony admin generator: is_published + AJAX

Часто приходится открывать запись на редактирование только ради того что бы поставить галочку "опубликовано"
Сделаем это нажатием на иконку статуса в списке.
Для работы с AJAX используем jQuery.
/backend/config/view.yml

default:
...
javascripts: [jq/jquery.pack.js]


/backend/templates/layout.php

<script>
jQuery(document).ready(function($){
$("body").after("<div id='result_div'></div>");
$("#result_div").html("<img style='padding: 20px' src='/images/loading.gif' align='absmiddle' /> обработка...");
$("#result_div").hide();
$("#result_div").css({
"background-color": "#ffffcc",
"opacity": "0.7",
"width": "40%",
"height": "auto",
"font-family": "Arial Tahoma",
"text-align": "center",
"vertical-align": "middle",
"border": "1px solid silver",
"position": "absolute",
"top": "40%",
"left": "30%",
"z-index": "9999"
});
});
</script>


Изменяем _is_published.php

<center>
<?php echo
link_to(
image_tag(sfConfig::get('sf_admin_web_dir')."/images/".
($myObject->getIsPublished() ? 'ok.png' : 'cancel.png'),
array('id' => 'switch_status_'.$myObject->getId())
),
"my_module/switchStatus?id=".$myObject->getId(),
array(
'class' => 'switch_status',
'rel' => 'switch_status_'.$myObject->getId(),
'pic_dir' => sfConfig::get('sf_admin_web_dir')."/images/",
)
);//link to
?>
</center>


Добавлем файл my_module/templates/_list_footer.php

<script>
$(document).ready(function() {
$(".switch_status").each( function (){
$(this).click( function (){
var popupX = Math.round( ($(window).width() - $("#result_div").width()) / 2) ;
var popupY = $(document).scrollTop() + Math.round($(window).height()/2) -
Math.round($("#result_div").height()/2);
$("#result_div").css({top: popupY+"px", left: popupX+"px"});
var pic_dir = $(this).attr("pic_dir")
var pic_id = $(this).attr("rel")
$("#result_div").slideDown("slow");

$.ajax({
type: "POST",
url: $(this).attr('href'),
data: "",
success: function(msg){
var pic_name = (msg == 1 ? "ok" : "cancel") + ".png"
$("#"+pic_id).attr("src", pic_dir + "/" + pic_name)
$("#result_div").slideUp("slow");
}
});//ajax
return false;
})//click
})//each
});
</script>


Добавляем новый метод executeSwitchStatus в модуль my_module

public function executeSwitchStatus($request){
$myObject = xxxPeer::retrieveByPk($this->getRequestParameter('id'));
$currentStatus = $myObject->getIsPublished();
$newStatus = ($currentStatus == 1 ? 0 : 1);

$myObject->setIsPublished($newStatus);
$myObject->save();

$isAjax = $request->isXmlHttpRequest();

if ($isAjax){
return $this->renderText($newStatus);
} else {
//do something if not AJAX
}//if
}//change status


Получаем

Symfony admin generator: маленькие улучшения

Очень часто в таблицах используется флажок is_published (bool)

Генератор админки его подхватывает, и ставит зеленую галочку, если is_published=1

Мы немного изменим такое поведение.
1. Создаем файл /backend/mymodule/templates/_is_published.php
и копируем в него следующее:

<center>
getIsPublished() ?
image_tag(sfConfig::get('sf_admin_web_dir').'/images/ok.png') :
image_tag(sfConfig::get('sf_admin_web_dir').'/images/cancel.png')
?>
</center>



В generator.yml

list:
fields:
...
is_published: { name: Статус }
...
display: [ ..., _is_published ]


Получаем

суббота, 11 октября 2008 г.

Symfony: Propel 1.3 installation

Не сразу заметен пробел после /1.3/

$ cd /path/to/project/root/
$ svn co http://svn.symfony-project.com/plugins/sfPropelPlugin/branches/1.3/[::space::]plugins/sfPropelPlugin

Пользователям "черепашки" обратить внимание.

вторник, 7 октября 2008 г.

Symfony: admin icons set

Незаменимая коллекция иконок для дополнительных кнопок, для админки и других интерфейсов.

famfam

Symfony: Propel unique validator


$this->validatorSchema->setPostValidator(new
sfValidatorPropelUnique(array(
'model' => 'Users',
'column' => array('login')),
array(
'invalid' => 'Такой логин уже есть.'
)
));

понедельник, 6 октября 2008 г.

Symfony custom log

Только для frontend.php получаем ошибку

Fatal error: Cannot redeclare class sfLogger in C:\www\Apache2\htdocs\sfprojects\XXX\cache\frontend\prod\config\config_core_compile.yml.php on line 1211

Лекарство:


require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');

$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', true);
sfContext::createInstance($configuration)->dispatch();

вторник, 16 сентября 2008 г.

Symfony: Создаем форму обратной связи 2/3

Приступим к генерации формы.

Начиная с версии 1.1 в Symfony для генерации форм используется система виджетов. Это означает что Вам надо будет в основном заниматься описанием полей и их свойств, и не заботиться о пользовательском вводе и проверке.

Теперь к практике.
В прошлой статье мы сгенерировали базовые классы форм командой >symfony propel:build-forms
Небольшое объяснение, Symfony генерирует базовые классы, в каталоге например
/lib/forms/base/BaseFeedbackLogForm.class.php
мы их не трогаем, мы в них будем "подглядывать", т.к. в следующий раз при выполнении
>symfony propel:build-forms
все классы в каталоге /lib/forms/base/ будут регенерированы заново, в соответствии в моделью, описанной в schema.yml, и все изменения внесенные в них будут уничтожены.

Для всех опытов и модернизаций нам предоставлены классы в каталоге /lib/forms/
Открываем класс /lib/forms/FeedbackLogForm.class.php, видим пустой класс, отложим его на время.

Открываем actions.class.php, и меняем метод executeIndex

public function executeIndex($request){
$this->form = new FeedbackLogForm(FeedbackLogPeer::retrieveByPk($request->getParameter('id')));
}

Запускаем http://minisite/frontend_dev.php/feedback
ops, error

Class "FeedbackUsers" must implement a "__toString" method to be rendered in a "sfWidgetFormPropelSelect" widget

Что произошло?
Symfony попыталась создать листбокс со списком получателей, для него используется "магический метод" __toString, но он не определен в модели, исправим это.

class FeedbackUsers extends BaseFeedbackUsers
{
public function __toString(){
return $this->getName();
}
}


Пробуем еще, и получаем такой экран:


Для начала неплохо, немного наведем красоту, открываем файл /lib/form/FeedbackLogForm.class.php и вносим следующие изменения:

class FeedbackLogForm extends BaseFeedbackLogForm
{
public function configure()
{
$this->setWidgets(array(
'id' => new sfWidgetFormInputHidden(),
'feedback_user_id' => new sfWidgetFormPropelSelect(array('model' => 'FeedbackUsers', 'add_empty' => true)),
'name' => new sfWidgetFormInput(),
'email' => new sfWidgetFormInput(),
'subject' => new sfWidgetFormInput(),
'content' => new sfWidgetFormTextarea(),
'get_answer' => new sfWidgetFormInputCheckbox(),
));

$this->widgetSchema->setLabels(array(
'feedback_user_id' => 'Кому',
'name' => 'Ваше Ф.И.О.',
'content' => 'Вопрос',
'email' => 'Ваш Email',
'subject' => 'Заголовок письма',
'get_answer' => 'Получить ответ на Email',
));
}//configure
}//class

Обратите внимание на $this->widgetSchema->setLabels тут мы меняем подписи к полям.

Что бы листбокс не пустовал, добавим пару записей.
ПО большому счету сайт начинается с админки.
Но мы пока не будем ей уделять много времени.
symfony propel:init-admin backend feedback_users FeedbackUsers
symfony propel:init-admin backend feedback_log FeedbackLog
Открываем http://minisite/backend_dev.php/feedback_users и вносим пару получателей.

Обновляем страницу http://minisite/frontend_dev.php/feedback и видим



Форма обрела более понятый вид, но нас не устраивает вид подписей к полям, мы хотим их выровнять по левому краю, также мы хотим поля снабдить коментариями, для обязательных полей поставим "*", и рядом с флажком напишем "да"
Для этого мы напишем воспользуемся директивой $this->widgetSchema->setHelps

$this->widgetSchema->setHelps(array(
'feedback_user_id' => ' * ',
'name' => ' * ',
'content' => ' * ',
'email' => ' * ',
'subject' => ' * ',
'get_answer' => ' да ',
));
$myDecorator = new sfFeedbackFormDecorator($this->getWidgetSchema());
$this->getWidgetSchema()->addFormFormatter('custom', $myDecorator);
$this->widgetSchema->setFormFormatterName('custom');

и напишем свой декоратор для строчек формы.

/*
* CUSTOM DECORATOR
*/
class sfFeedbackFormDecorator extends sfWidgetFormSchemaFormatter {
protected
$rowFormat = "
%hidden_fields%

%label%
%field% %help%
\n",
$helpFormat = '%help%',
$errorRowFormat = "
\n%errors%
\n",
$errorListFormatInARow = " \n%errors% \n",
$errorRowFormatInARow = " %error%\n",
$namedErrorRowFormatInARow = "
  • %name%: %error%
  • \n",
    $decoratorFormat = "";
    }// decorator class


    Обновляем страницу и видим:


    На сегодня хватит.
    Что мы пока имеем? Никаких очевидных преимуществ ;)
    Такую формочку с использованием классического HTML мы бы сделали гораздо быстрее, но не будем торопиться, завтра попробуем проверять введенные данные и сохранять их в БД.

    понедельник, 15 сентября 2008 г.

    Symfony: Создаем форму обратной связи 1/3

    Начнем делать маленький сайт-визитку с использованием Symfony(версия 1.1.1 на момент написания статьи).

    Первым модулем сделаем обратную связь.

    Обратная связь необходимый компонент практически для любого сайта, на ее примере мы опробуем многие интересные моменты.

    Приступим.

    1. Создаем проект

    >symfony init-project minisite

    2. Создаем 2 приложения

    >symfony init-app frontend
    >symfony init-app backend

    3. Разворачиваем базу
    ссылка на дамп

    4. Пишем схему

    propel:

    sf_feedback_users:
    _attributes: { phpName: FeedbackUsers }
    id: { type: integer, required: true,primaryKey: true, autoIncrement: true }
    name: { type: varchar(255) }
    email: { type: varchar(255) }
    subject: { type: varchar(255) }

    sf_feedback_log:
    _attributes: { phpName: FeedbackLog }
    id: { type: integer, required: true, primaryKey: true, autoIncrement: true }
    feedback_user_id: { phpName: FeedbackUserId, type: integer, foreignTable: sf_feedback_users, foreignReference: id }
    name: { type: varchar(255) }
    email: { type: varchar(255) }
    subject: { type: varchar(255) }
    content: { type: longvarchar }
    is_published: { type: boolean, default: 0 }
    get_answer: { type: boolean, default: 0 }
    created_at: ~
    updated_at: ~


    Проверим propel.ini, вписываем свои настройки для MySQL
    propel.targetPackage = lib.model
    propel.packageObjectModel = true
    propel.project = minisite
    propel.database = mysql
    propel.database.createUrl = mysql://root@localhost/
    propel.database.url = mysql://root@localhost/minisite

    5. Создаем модель
    >symfony propel:build-model

    6. Сгенерируем формы
    >symfony propel:build-forms

    7. Вносим поправки в C:\WINDOWS\system32\drivers\etc\hosts
    127.0.0.1 minisite

    8. Вносим изменения в httpd.conf
    ServerName minisite
    DocumentRoot "C:\www\sfprojects\minisite\web"
    DirectoryIndex index.php
    Alias /sf c:\php\data\symfony\web\sf

    AllowOverride All
    Allow from All


    AllowOverride All
    Allow from All



    Перезапускаем Апач.

    7. Проверяем
    http://minisite/index.php



    Если Вы видите такое изображение, значит все идет по плану :)

    8. Создаем модуль обр.связи
    >symfony init-module frontend feedback

    Проверяем:
    http://minisite/index.php/feedback


    Если все сделано правильно, видим ту же картинку, только надпись по центру изменилась. Все идет хорошо, в следующей статье займемся непосредственной генерацией формы.

    пятница, 29 августа 2008 г.

    Установка Symfony

    Порядок установки фреймворка SYMFONY 1.1.1 на Windows-based-PC, PHP 5.2+

    1. Активируйе PEAR: C:\PHP\GO-PEAR.BAT, соглашайтесь со всем, что попросит инсталятор. В конце, не забудьте внести обновление в реестр: C:\PHP\PEAR_ENV.reg

    2. На всякий случай запустите обновление C:\PHP\PEAR UPGRADE-ALL

    3. Подключаем канал SYMFONY C:\PHP\PEAR channel-discover pear.symfony-project.com

    4. Инсталируем SYMFONY C:\PHP\PEAR install symfony/symfony-1.1.1. На медленных машинах процесс может занять 10-15 минут, неберитесь терпения, дождитесь сообщения
    install ok: channel://pear.symfony-project.com/symfony-1.1.1

    5. Проверяем установку
    C:\PHP>symfony -V
    symfony version 1.1.1 (C:\PHP\PEAR\symfony)

    Поздравляю, фреймворк установлен!
    Его библиотеки располагаются в каталоге C:\PHP\PEAR\symfony.
    Они будут доступны всем проектам, что Вы напишите на Symfony.
    При выходе новой версии фреймворка Symfony Вам достаточно будет выполнить команду обновления: C:\PHP\PEAR upgrate symfony/symfony.

    Метод инсталяции через PEAR довольно удобен, и хорош тем, что библиотеки фреймворка изолированы от скриптов проекта, что повышает надежность. С другой стороны есть и недостаток, при переходе на новую версию фреймворка, некоторые проекты могут оказаться неработоспособными, из-за изменений в ядре фремворка, а так же могут потребоваться более новые версии дополнений-"плагинов", которые выходят как правило с запозданием.
    Что бы избежать этого, существует процедура "заморозки" проекта, но о ней позднее.

    Существует и другой метод установки фреймворка - метод установки "песочницы", о нем в следующей статье.

    Спасибо за внимание, удачи!