Показаны сообщения с ярлыком symfony. Показать все сообщения
Показаны сообщения с ярлыком symfony. Показать все сообщения

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

Symfony: Doctrine slugify кириллица

/lib/Slugify.class.php

class SlugifyClass {
  static function Slugify($title) {
    $gost = array(
     "Є"=>"EH","І"=>"I","і"=>"i","№"=>"#","є"=>"eh",
     "А"=>"A","Б"=>"B","В"=>"V","Г"=>"G","Д"=>"D",
     "Е"=>"E","Ё"=>"JO","Ж"=>"ZH",
     "З"=>"Z","И"=>"I","Й"=>"JJ","К"=>"K","Л"=>"L",
     "М"=>"M","Н"=>"N","О"=>"O","П"=>"P","Р"=>"R",
     "С"=>"S","Т"=>"T","У"=>"U","Ф"=>"F","Х"=>"KH",
     "Ц"=>"C","Ч"=>"CH","Ш"=>"SH","Щ"=>"SHH","Ъ"=>"'",
     "Ы"=>"Y","Ь"=>"","Э"=>"EH","Ю"=>"YU","Я"=>"YA",
     "а"=>"a","б"=>"b","в"=>"v","г"=>"g","д"=>"d",
     "е"=>"e","ё"=>"jo","ж"=>"zh",
     "з"=>"z","и"=>"i","й"=>"jj","к"=>"k","л"=>"l",
     "м"=>"m","н"=>"n","о"=>"o","п"=>"p","р"=>"r",
     "с"=>"s","т"=>"t","у"=>"u","ф"=>"f","х"=>"kh",
     "ц"=>"c","ч"=>"ch","ш"=>"sh","щ"=>"shh","ъ"=>"",
     "ы"=>"y","ь"=>"","э"=>"eh","ю"=>"yu","я"=>"ya","«"=>"","»"=>"","—"=>"-"," "=>"-"
    );

    $iso = array(
     "Є"=>"YE","І"=>"I","Ѓ"=>"G","і"=>"i","№"=>"#","є"=>"ye","ѓ"=>"g",
     "А"=>"A","Б"=>"B","В"=>"V","Г"=>"G","Д"=>"D",
     "Е"=>"E","Ё"=>"YO","Ж"=>"ZH",
     "З"=>"Z","И"=>"I","Й"=>"J","К"=>"K","Л"=>"L",
     "М"=>"M","Н"=>"N","О"=>"O","П"=>"P","Р"=>"R",
     "С"=>"S","Т"=>"T","У"=>"U","Ф"=>"F","Х"=>"X",
     "Ц"=>"C","Ч"=>"CH","Ш"=>"SH","Щ"=>"SHH","Ъ"=>"'",
     "Ы"=>"Y","Ь"=>"","Э"=>"E","Ю"=>"YU","Я"=>"YA",
     "а"=>"a","б"=>"b","в"=>"v","г"=>"g","д"=>"d",
     "е"=>"e","ё"=>"yo","ж"=>"zh",
     "з"=>"z","и"=>"i","й"=>"j","к"=>"k","л"=>"l",
     "м"=>"m","н"=>"n","о"=>"o","п"=>"p","р"=>"r",
     "с"=>"s","т"=>"t","у"=>"u","ф"=>"f","х"=>"x",
     "ц"=>"c","ч"=>"ch","ш"=>"sh","щ"=>"shh","ъ"=>"",
     "ы"=>"y","ь"=>"","э"=>"e","ю"=>"yu","я"=>"ya","«"=>"","»"=>"","—"=>"-"," "=>"-"
    );

    $rtl_standard = sfConfig::get('rtl_standard', 'gost') ;

    //to lower case

    $title = mb_strtolower($title, 'UTF-8');
    
    switch ($rtl_standard) {
      case 'off':
          return $title;
      case 'gost':
          $out = trim(strtr($title, $gost), '-');
      default:
          $out = trim(strtr($title, $iso), '-');
    }
    
    // clean slug
    $out = preg_replace('/[^a-z0-9\-]+/i', '', $out);
    return $out;
  }
}



Использование

Post:
  actAs:
    Timestampable: ~
    Taggable: ~
    Sluggable:
      unique: true
      fields: [title]
      canUpdate: true
      builder: [SlugifyClass, Slugify]

вторник, 29 сентября 2009 г.

Symfony: Doctrine order by calculated field

Положим нам нужно вывести 10 наиболее комментируемых постов.
Кусок схемы:
Post:
tableName: blog_post
actAs:
Timestampable: ~
Taggable: ~
columns:
id: { type: integer(4), primary: true, autoincrement: true }
title: { type: string(255) }
extract: { type: string(1024) }
content: { type: string(4096) }
is_published: { type: boolean, default: false }

Comment:
tableName: blog_comment
actAs:
Timestampable: ~
columns:
id: { type: integer(4), primary: true, autoincrement: true }
post_id: { type: integer(4), notnull: true }
name: { type: string(100) }
email: { type: string(100) }
content: { type: string(4096) }
subscribe: { type: boolean, default: false }
relations:
Post:
class: Post
local: post_id
foreign: id
foreignAlias: PostComments
type: one
foreignType: many
onDelete: CASCADE


Получим популярные посты одним запросом:
  public function executeGetMostCommentedBlogPosts()
{
$q = Doctrine_Query::create()
->select('p.*')
->addSelect('(SELECT count(*) FROM PostComments pc WHERE pc.post_id = p.id) as comments_count')
->from('Post p')
->where('p.is_published', true)
->orderBy('comments_count DESC')
->addOrderBy('d.created_at DESC')
->limit(10);

$this->posts = $q->execute();
}

понедельник, 24 августа 2009 г.

Symfony: date selector (выбор даты рождения)

Нас не устраивает стандартный выбор даты в виде 3х листбоксов в формате месяц/день/год, да еще и месяцы указаны цифрами, что вызывает массу затруднений.
Мы хотим такой вид:


Решение:
  public function setup()
{
$years = range(1950, 2000); //Creates array of years between 1950-2000
$years_list = array_combine($years, $years); //Creates new array where key and value are both values from $years list

$this->setWidgets(array(
'id' => new sfWidgetFormInputHidden(),
'city' => new sfWidgetFormInput(),
'birthday' => new sfWidgetFormI18nDate(
array(
'culture' => 'ru',
'format' => '%day%.%month%.%year%',
'years' => $years_list,
),
array()
),

Symfony: sfWidgetFormInputFileEditable IE broken image

Положим, что мы хотим позволить пользователю прикреплять картинку к своему профилю.
Обратившись к JOBEET находим решение - виджет sfWidgetFormInputFileEditable
Используем его в своей форме:
    
$this->setWidgets(array(
'id' => new sfWidgetFormInputHidden(),
...
'avatar' => new sfWidgetFormInputFileEditable(array(
'label' => 'Ваш аватар',
'file_src' => '/uploads/avatars/'.$this->getObject()->getAvatar(),
'is_image' => true,
'edit_mode' => !$this->isNew(),
)),

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


Решение проблемы:
   'avatar'       => new sfWidgetFormInputFileEditable(array(
'label' => 'Avatar for forum, blog, comments, etc...',
'file_src' => '/uploads/avatars/'.$this->getObject()->getAvatar(),
'is_image' => true,
'edit_mode' => ($this->getObject()->getAvatar() OR $this->isNew() ? true: false),
)),

Ну и заинтересовавшимся для комплекта валидатор:
    $this->validatorSchema['avatar'] = new sfValidatorFile(array(
'required' => false,
'path' => sfConfig::get('sf_upload_dir').'/avatars',
'mime_types' => 'web_images',
'max_size' => '40960', //(40KB in bytes)
));

понедельник, 18 мая 2009 г.

Symfony: applications as subdomains

Мы хотим иметь такую конструкцию:
site.com             = frontend.php
www.site.com = frontend.php
dev.www.site.com = frontend_dev.php
backend.site.com = backend.php
dev.backend.site.com = backend_dev.php


Изменяем index.php

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

// get the domain parts as an array
@list($tld, $domain, $subdomain, $subdomain2) = array_reverse(explode('.', $_SERVER['HTTP_HOST']));

// determine which subdomain we're looking at
$app = ($subdomain == 'webadmin') ? 'backend' : $subdomain;
$app = (empty($app) || $app == 'www' ) ? 'frontend' : $app;
$env = (empty($subdomain2) || $subdomain2 == 'www') ? 'prod' : $subdomain2;

// determine which app to load based on subdomain
if (!is_dir(realpath(dirname(__FILE__).'/..').'/apps/'.$app))
{
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', false);
}
else
{
$configuration = ProjectConfiguration::getApplicationConfiguration($app, $env, false);
}

sfContext::createInstance($configuration)->dispatch();


Использованные материалы: Dynamically Loading Symfony Applications Via Subdomains

четверг, 23 апреля 2009 г.

Symfony: raw query, get years list for active posts

public function executeIndex(sfWebRequest $request)
{
//get years
$connection = Propel::getConnection();
$query = '
SELECT YEAR( %s ) AS myYear
FROM %s
WHERE %s = 1
GROUP BY YEAR( %s )
ORDER BY myYear DESC
';

$query = sprintf($query,
PostPeer::PUBLISHED_AT,
PostPeer::TABLE_NAME,
PostPeer::IS_PUBLISHED,
PostPeer::PUBLISHED_AT
);
$statement = $connection->prepare($query);
$statement->execute();
$years = array();
while($resultset = $statement->fetch(PDO::FETCH_OBJ))
{
$years[] = $resultset->myYear;
}
$this->years = $years;
}


returns array(2009, 2008, 2007...)

понедельник, 13 апреля 2009 г.

Symfony: filter by 2 fields in backend

Простая ситуация: в таблице 2 поля
  sf_post:
_attributes: { phpName: Post }
id: ~
title: { type: varchar(255) }
content: { type: longvarchar }
...

Нам не важно в заголовке или в описании есть искомое поле, мы хотим в админке сделать фильтрацию по обоим полям. Берем исходник из кэша и модифицируем /backend/modules/post/actions/actions.class.php
  protected function buildCriteria()
{
$filters = $this->getFilters();
if (is_null($this->filters))
{
$this->filters = $this->configuration->getFilterForm($this->getFilters());
}

$criteria = $this->filters->buildCriteria($this->getFilters());

$this->addSortCriteria($criteria);

$event = $this->dispatcher->filter(new sfEvent($this, 'admin.build_criteria'), $criteria);
$criteria = $event->getReturnValue();

if (!empty($filters['title']['text']))
{
//добавим так же фильтр по полю CONTENT
$c1 = $criteria->getNewCriterion(PostPeer::TITLE, '%'.$filters['title']['text'].'%', Criteria::LIKE);
$c2 = $criteria->getNewCriterion(PostPeer::CONTENT, '%'.$filters['title']['text'].'%', Criteria::LIKE);
$c1->addOr($c2);
$criteria->add($c1);
}

return $criteria;
}

среда, 8 апреля 2009 г.

Symfony 1.2 tabular(embedded) subforms

Довольно часто при редактировании требуется вывести N дочерних записей в виде таблицы.
К сожалению стандартными средствами Symfony этого сделать не получилось, в первую очередь из за того, что Symfony в каждой строке оставляет место для сообщений об ошибках валидации.
Постараемся с минимумом усилий сделать таблицу с подчиненными записями.
Для примера возьмем очень простую ситуацию - в нашем интернет-магазине каждый товар может иметь 0-N рекомендованных товаров.

Кусок схемы:

propel:
pc_product:
_attributes: { phpName: Product }
id: ~
name: varchar(255)
price1: float
price2: float
description: longvarchar
pic: varchar(255)
is_published: boolean
category_id: { type: integer, foreignReference: id, foreignTable: pc_category, onDelete: cascade, onUpdate: cascade, required: true }

pc_product_recommended:
_attributes: { phpName: ProductRecommended }
id: ~
product_id: { type: integer, foreignReference: id, foreignTable: pc_product, onDelete: cascade, onUpdate: cascade, required: true }
recommend_product_id: { type: integer, foreignReference: id, foreignTable: pc_product, onDelete: cascade, onUpdate: cascade, required: true }
note: { type: varchar(512), default: null }
pos: integer

pc_category:
_attributes: { phpName: Category, treeMode: NestedSet }
id: ~
name: varchar(255)
alias: varchar(1024)
lft: { type: integer, required: false, default: 0, nestedSetLeftKey: 'true'}
rgt: { type: integer, required: false, default: 0, nestedSetRightKey: 'true'}
scope: { type: integer, required: false, default: 0, treeScopeKey: 'true'}
parent: { type: integer }
is_published: { type: boolean }
deleted_at: { type: date }


Изменяем /lib/forms/ProductForm.class.php

class ProductForm extends BaseProductForm
{
public function configure()
{
$this->setWidgets(array(
'category_id' => new sfWidgetFormPropelChoice(array('model' => 'Category', 'add_empty' => false)),
'id' => new sfWidgetFormInputHidden(),
'name' => new sfWidgetFormInput(),
'price1' => new sfWidgetFormInput(),
'price2' => new sfWidgetFormInput(),
'pic' => new myWidgetFormInputFileEditable(),
'is_published' => new sfWidgetFormInputCheckbox(),
));


$this->setValidators(array(
'id' => new sfValidatorPropelChoice(array('model' => 'Product', 'column' => 'id', 'required' => false)),
'name' => new sfValidatorString(array('max_length' => 255, 'required' => false)),
'price1' => new sfValidatorNumber(array('required' => false)),
'price2' => new sfValidatorNumber(array('required' => false)),
'is_published' => new sfValidatorBoolean(array('required' => false)),
'category_id' => new sfValidatorPropelChoice(array('model' => 'Category', 'column' => 'id')),
'pic' => new sfValidatorFile(array(
'required' => false,
'max_size' => '102400', // bytes (1MB)
'mime_types' => array('image/png', 'image/jpeg', 'image/gif',)
)),
));

$this->widgetSchema->setNameFormat('product[%s]');

$this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema);

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

//получим дочерние записи
$c = new Criteria();
$c->add(ProductRecommendedPeer::PRODUCT_ID, $this->getObject()->getId());
$c->addAscendingOrderByColumn(ProductRecommendedPeer::ID);
$rps = ProductRecommendedPeer::doSelect($c);
//цикл по дочерним записям
foreach ($rps as $index => $rProduct)
{
$rProductForm = new ProductRecommendedForm($rProduct);
$rProductForm->setWidget('product_id', new sfWidgetFormInputHidden());
$rProductForm->setWidget('del_me', new sfWidgetFormInputCheckbox());
$rProductForm->setValidator('del_me', new sfValidatorBoolean(array('required' => false)));
$rProductForm->widgetSchema->moveField('del_me', sfWidgetFormSchema::FIRST);

$this->embedForm('rProduct-'.$index, $rProductForm);
$this->widgetSchema->setLabel('rProduct-'.$index, $index+1);
}
$rProductForm = new ProductRecommendedForm();

$rProductForm->setWidget('product_id', new sfWidgetFormInputHidden());
//добавляем дополнительный чекбокс, для удаления записи
$rProductForm->setWidget('del_me', new sfWidgetFormInputCheckbox(
array(),
array('disabled' => true)
));
if (!$this->getObject()->isNew())
{
//добавим пустой элемент в списке, на случай если мы не хотим добавлять новую запись
$rProductForm->setWidget('recommend_product_id', new sfWidgetFormPropelChoice(
array('model' => 'Product', 'add_empty' => true)));

$rProductForm->setDefaults(array('product_id' => $this->getObject()->getId()));
//отключим обязательный выбор значения листбокса
$rProductForm->setValidator('recommend_product_id', new sfValidatorInteger(array('required' => false)));
//переносим чекбокс для удаления записи наверх
$rProductForm->widgetSchema->moveField('del_me', sfWidgetFormSchema::FIRST);

$this->embedForm('new-rec', $rProductForm);
$this->widgetSchema->setLabel('new-rec', 'new record');
}


}

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);
}

}



Если в /generator.yml настройки форм по умолчанию:

form: ~
edit: ~
new: ~

мы видим внизу формы редактирования доп.форму с добавлением записи

она даже работает ;) но внешний вид нас не устраивает, и по работе тоже есть нарекания, при каждом обновлении родительской формы без указания нового рекомендованного товара, у нас добавляется пустая запись, а так же еще не работает удаление записей.

Давайте исправим внешний вид и логику добавления-удаления.

Редактируем в config/generator.yml настройки формы указываем поля для отображения:

form:
display: [ name, price1, price2, pic, is_published ]

Такая небольшая настройка приводит к тому, что исчезают наши встроенные формы, подробнее об этом явлении как нибудь позже...
При этом форма перестает работать, что неудивительно, т.к. мы к форме прикрепили массу валидаторов для подчиненных форм.
Теперь мы в ручную отобразим подформы в табличном виде.
Заходим в кэш и копируем
/cache/backend/.../autoProduct/templates/_form.php в
apps/backend/modules/product/templates/_form.php

Далее заменяем содежимое этого файла на:

<div class="sf_admin_form">
<?php echo form_tag_for($form, '@product') ?>
<?php echo $form->renderHiddenFields() ?>

<?php if ($form->hasGlobalErrors()): ?>
<?php echo $form->renderGlobalErrors() ?>
<?php endif; ?>

<?php foreach ($configuration->getFormFields($form, $form->isNew() ? 'new' : 'edit') as $fieldset => $fields): ?>
<?php include_partial('product/form_fieldset', array('product' => $product, 'form' => $form, 'fields' => $fields, 'fieldset' => $fieldset)) ?>
<?php endforeach; ?>

<?php if ($form->getEmbeddedForms()): ?>
<fieldset id="sf_fieldset_recommended_products">
<h2>Recommended products</h2>
<table>
<tr>
<th>#</th>
<th>del</th>
<th>product</th>
<th>note</th>
<th>pos</th>
</tr>
<?php foreach ($form->getEmbeddedForms() as $index => $embForm): ?>
<tr>
<?php echo $form[$index]['id']->render() ?>
<?php echo $form[$index]['product_id']->render() ?>
<td><?php echo $form[$index]->renderLabel() ?></td>
<td><?php echo $form[$index]['del_me']->renderError() ?><?php echo $form[$index]['del_me']->render() ?></td>
<td><?php echo $form[$index]['recommend_product_id']->renderError() ?><?php echo $form[$index]['recommend_product_id']->render() ?></td>
<td><?php echo $form[$index]['note']->renderError() ?><?php echo $form[$index]['note']->render() ?></td>
<td><?php echo $form[$index]['pos']->renderError() ?><?php echo $form[$index]['pos']->render() ?></td>
</tr>
<?php endforeach; ?>
</table>
</fieldset>
<?php endif; ?>

<?php include_partial('product/form_actions', array('product' => $product, 'form' => $form, 'configuration' => $configuration, 'helper' => $helper)) ?>
</form>
</div>



Подформа преобрела более приятный вид, остается разобраться с обновлением и удалением.
Добавляем новый метод в /lib/forms/ProductForm.class.php

public function updateObjectEmbeddedForms($values, $forms = null)
{
if (is_null($forms))
{
$forms = $this->embeddedForms;
}

foreach ($forms as $name => $form)
{
if (!is_array($values[$name]))
{
continue;
}

if ($form instanceof sfFormPropel)
{
if (isSet($values[$name]['del_me'])){
if ($values[$name]['del_me'] == 1)
{
$form->getObject()->delete();
unset($this->embeddedForms[$name]);
continue;
}
} else {
if (strstr($name, 'new-'))
{
//именно тут мы сами выступаем в роли валидатора, и если одно из обязательных полей пустое,
//мы игнорируем новую запись
$rProduct = $values[$name]['recommend_product_id'];
$rPos = $values[$name]['pos'];
if ( empty($rProduct) OR empty($rPos))
{
unset($this->embeddedForms[$name]);
continue;
}
}
}//if !isset del me
$form->updateObject($values[$name]);
}
else
{
$this->updateObjectEmbeddedForms($values[$name], $form->getEmbeddedForms());
}
}
//die();
}//updateObjectEmbeddedForms

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

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>

среда, 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...

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

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

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

среда, 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


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