Professional Documents
Culture Documents
Symfony
Clase 2
Javier Eguíluz
javier.eguiluz@gmail.com
Esta obra dispone de una licencia de tipo Creative
Commons Reconocimiento‐No comercial‐ Compartir
bajo la misma licencia 3.0
Se prohíbe explícitamente el uso de este material en
actividades de formación comerciales
http://creativecommons.org/licenses/by‐nc‐sa/3.0/es/
This work is licensed under a Creative Commons
Attribution‐Noncommercial‐Share Alike 3.0
http://creativecommons.org/licenses/by‐nc‐sa/3.0/es/
Capítulo 6
Profundizando en
el modelo
El objeto Criteria
de Propel
apps/frontend/modules/job/actions/actions.class.php
{
$criteria = new Criteria();
$criteria‐>add(
JobeetJobPeer::COMPANY,
'Empresa ACME'
);
$this‐>listado = JobeetJobPeer::doSelect($criteria);
}
Depurando el código
SQL generado
log/frontend_dev.log
Dec 6 15:47:12 symfony [debug] {sfPropelLogger} exec: SET NAMES 'utf8‘
Dec 6 15:47:12 symfony [debug] {sfPropelLogger} prepare: SELECT
jobeet_job.ID, jobeet_job.CATEGORY_ID, jobeet_job.TYPE,
jobeet_job.COMPANY, jobeet_job.LOGO, jobeet_job.URL,
jobeet_job.POSITION, jobeet_job.LOCATION, jobeet_job.DESCRIPTION,
jobeet_job.HOW_TO_APPLY, jobeet_job.TOKEN, jobeet_job.IS_PUBLIC,
jobeet_job.CREATED_AT, jobeet_job.UPDATED_AT FROM ''jobeet_job''
WHERE jobeet_job.CREATED_AT>:p1
SQL Injection
Dec 6 15:47:12 symfony [debug] {sfPropelLogger} Binding '2008‐11‐06
15:47:12' at position :p1 w/ PDO type PDO::PARAM_STR
Serializando objetos
lib/model/JobeetJob.php
return parent::save($con);
}
}
apps/frontend/modules/job/actions/actions.class.php
$this‐>listado = JobeetJobPeer::doSelect(
$criteria
);
}
Personalizando la
configuración
¿Dónde está el problema?
public function executeIndex(sfWebRequest $request)
{
$criteria = new Criteria();
$criteria‐>add(
JobeetJobPeer::CREATED_AT,
time() ‐ 86400 * 30,
mejor como opción
de configuración
Criteria::GREATER_THAN
);
$this‐>listado = JobeetJobPeer::doSelect($criteria);
}
apps/frontend/config/app.yml
all:
dias_activa: 30
sfConfig::get('app_dias_activa')
Refactorización
¿Dónde está el problema?
Controlador (MVC)
public function executeIndex(sfWebRequest $request)
{
$criteria = new Criteria();
$criteria‐>add(
JobeetJobPeer::CREATED_AT, Modelo (MVC)
time() ‐ 86400 * 30,
Criteria::GREATER_THAN
);
$this‐>listado = JobeetJobPeer::doSelect($criteria);
}
Modelo lib/model/JobeetJobPeer.php
class JobeetJobPeer extends BaseJobeetJobPeer {
static public function getActiveJobs() {
$criteria = new Criteria();
$criteria‐>add(
self::EXPIRES_AT,
time(),
Criteria::GREATER_THAN
);
return self::doSelect($criteria);
}
}
apps/frontend/modules/job/templates/indexSuccess.php
<?php foreach ($categories as $category): ?>
...
<h1><?php echo $category ?></h1>
...
<?php foreach ($category‐>getActiveJobs() as $i => $job): ?>
...
<?php endforeach; ?>
...
<?php endforeach; ?>
Controlador Vista Modelo
getWithJobs()
1
JobeetCategoryPeer.php
getActiveJobs()
3
2 4 getActiveJobs()
$categories
JobeetJobPeer.php
Limitando los
resultados
lib/model/JobeetJobPeer.php
return self::doSelect($criteria);
}
}
apps/frontend/modules/job/templates/indexSuccess.php
apps/frontend/config/app.yml
all:
active_days: 30
max_jobs_on_homepage: 10
Archivos de datos
dinámicos
data/fixtures/020_jobs.yml
JobeetJob:
<?php for ($i = 100; $i <= 130; $i++): ?>
job_<?php echo $i ?>:
category_id: programming
company: Company <?php echo $i."\n" ?>
position: Web Developer
location: Paris, France
description: |
Lorem ipsum dolor sit amet,
consectetur adipisicing elit.
how_to_apply: |
Send your resume to lorem.ipsum
[at] company_<?php echo $i ?>.sit
is_public: true
is_activated: true
token: job_<?php echo $i."\n" ?>
email: job@example.com
<?php endfor; ?>
Restringir el acceso a
una oferta de trabajo
apps/frontend/config/routing.yml
job_show_user:
url: /job/:company_slug/:location_slug/:id/:position_slug
class: sfPropelRoute
options:
model: JobeetJob
type: object
method_for_criteria: doSelectActive
param: { module: job, action: show }
requirements:
id: \d+
sf_method: [get]
lib/model/JobeetJobPeer.php
La página de cada
categoría
La ruta de la
categoría
apps/frontend/config/routing.yml
category:
url: /category/:slug
class: sfPropelRoute
param: { module: category, action: show }
options: { model: JobeetCategory, type: object }
lib/model/JobeetCategory.php
$total = $category‐>countActiveJobs() ‐
sfConfig::get('app_max_jobs_on_homepage')
propel:
jobeet_category:
id: ~
name: { type: varchar(255), required: true }
slug:
type: varchar(255)
required: true
index: unique
getSlug()
lib/model/JobeetCategory.php
$this‐>setSlug(Jobeet::slugify($name));
}
$ ./symfony propel:build‐all‐load
apps/frontend/modules/category/actions/actions.class.php
class categoryActions extends sfActions
{
public function executeShow(sfWebRequest $request)
{
$this‐>category = $this‐>getRoute()‐>getObject();
}
}
apps/frontend/modules/category/templates/showSuccess.php
“trozos de código de plantilla que se
pueden reutilizar en varias plantillas”
Son iguales que las plantillas en todo salvo que
su nombre empieza por un guión bajo (_)
apps/frontend/modules/job/templates/_list.php
<table class="jobs">
<?php foreach ($jobs as $i => $job): ?>
<tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>">
<td class="location">
<?php echo $job‐>getLocation() ?>
</td>
<td class="position">
<?php echo link_to($job‐>getPosition(),
'job_show_user',
$job) ?>
</td>
<td class="company">
<?php echo $job‐>getCompany() ?>
</td>
</tr>
<?php endforeach; ?>
</table>
apps/frontend/modules/job/templates/indexSuccess.php
<?php include_partial(
'job/list',
array('jobs' => $category‐>getActiveJobs(
sfConfig::get('app_max_jobs_on_homepage')
))
) ?>
apps/frontend/modules/job/templates/showSuccess.php
<?php include_partial(
'job/list',
array('jobs' => $category‐>getActiveJobs())
) ?>
Paginación
apps/frontend/modules/category/actions/actions.class.php
Pruebas unitarias
test/
unit/
Prueban funciones y
métodos individualmente
functional/
Prueban la aplicación en
su conjunto
El framework de
pruebas lime
require_once dirname(__FILE__).'/../bootstrap/unit.php';
$t = new lime_test(1, new lime_output_color());
número de pruebas
esperadas
ok($condicion)
is($valor1, $valor2)
isnt($valor1, $valor2)
like($cadena, $expresionRegular)
unlike($cadena, $expresionRegular)
is_deeply($array1, $array2)
Ejecutando pruebas
unitarias
test/unit/JobeetTest.php
require_once dirname(__FILE__).'/../bootstrap/unit.php';
$ ./symfony test:unit Jobeet
Probando el método
slugify()
Sensio Labs sensio‐labs
Paris, France paris‐france
test/unit/JobeetTest.php
require_once dirname(__FILE__).'/../bootstrap/unit.php';
$t‐>is(Jobeet::slugify('Sensio'), 'sensio');
$t‐>is(Jobeet::slugify('sensio labs'), 'sensio‐labs');
$t‐>is(Jobeet::slugify('sensio labs'), 'sensio‐labs');
$t‐>is(Jobeet::slugify('paris,france'), 'paris‐france');
$t‐>is(Jobeet::slugify(' sensio'), 'sensio');
$t‐>is(Jobeet::slugify('sensio '), 'sensio');
test/unit/JobeetTest.php
require_once dirname(__FILE__).'/../bootstrap/unit.php';
$t‐>comment('::slugify()');
$t‐>is(Jobeet::slugify('Sensio'), 'sensio',
'::slugify() pasa la cadena de texto a minúsculas');
$t‐>is(Jobeet::slugify('sensio labs'), 'sensio‐labs',
'::slugify() sustituye los espacios en blanco por ‐');
...
Pruebas unitarias
para Propel
$ mysqladmin ‐uroot ‐p create jobeet_test
$ symfony configure:database ‐‐env=test
"mysql:host=localhost;dbname=jobeet_test"
root ConTraSenA
config/databases.yml
config/databases.yml
dev:
propel:
class: sfPropelDatabase
param:
classname: DebugPDO
test:
propel:
class: sfPropelDatabase
param:
classname: DebugPDO
dsn: 'mysql:host=localhost;dbname=jobeet_test'
all:
propel:
class: sfPropelDatabase
param:
dsn: 'mysql:host=localhost;dbname=jobeet'
username: root
password: null
test/bootstrap/propel.php
include(dirname(__FILE__).'/unit.php');
$configuration = ProjectConfiguration::getApplicationConfiguration(
'frontend',
'test',
true
);
new sfDatabaseManager($configuration);
include(dirname(__FILE__).'/../../bootstrap/propel.php');
$t‐>comment('‐>getCompanySlug()');
$job = JobeetJobPeer::doSelectOne(new Criteria());
$t‐>is(
$job‐>getCompanySlug(),
Jobeet::slugify($job‐>getCompany()),
'‐>getCompanySlug() devuelve el slug del nombre de la empresa'
);
Conjuntos de
pruebas unitarias
$ ./symfony test:unit
Capítulo 9
Pruebas
funcionales
La clase sfBrowser
sfBrowser
servidor web
aplicación aplicación
Symfony Symfony
get() reload() setHttpHeader()
post() click() setAuth()
call() select() setCookie()
back() deselect() removecookie()
forward() restart() clearCookie()
followRedirect()
$browser = new sfBrowser();
$browser‐>
get('/')‐>
click('Design')‐>
get('/category/programming?page=2')‐>
get('/category/programming', array('page' => 2))‐>
post('search', array('keywords' => 'php'))
;
La clase
sfTestFunctional
sfBrowser
sfTestFunctional
$browser‐>
get('/category/index')‐>
with('request')‐>begin()‐>
isParameter('module', 'category')‐>
isParameter('action', 'index')‐>
end()‐>
with('response')‐>begin()‐>
isStatusCode(200)‐>
checkElement('body', '!/This is a temporary page/')‐>
end()
;
interfaz fluída
$ symfony test:all
Capítulo 10
Los formularios
1. Crear código HTML del formulario
2. Definir reglas de validación para los datos
3. Procesar valores enviados por el usuario
4. Guardar la información en la base de datos
5. Mostrar posibles mensajes de error
6. Volver a mostrar los datos en el formulario
Symfony ya incluye...
• Validación (para cada campo)
• Widgets (campos del formulario)
$this‐>setValidators(array(
'email' => new sfValidatorEmail(),
'message' => new sfValidatorString(array(
'max_length' => 255)
),
));
}
}
sfWidgetFormChoice sfWidgetFormInputHidden
sfWidgetFormChoiceMany sfWidgetFormInputPassword
sfWidgetFormDate sfWidgetFormPropelChoice
sfWidgetFormDateRange sfWidgetFormPropelChoiceMany
sfWidgetFormDateTime sfWidgetFormPropelSelect
sfWidgetFormFilterDate sfWidgetFormPropelSelectMany
sfWidgetFormFilterInput sfWidgetFormSchema
sfWidgetFormI18nDate sfWidgetFormSchemaDecorator
sfWidgetFormI18nDateTime sfWidgetFormSchemaForEach
sfWidgetFormI18nSelectCountry sfWidgetFormSchemaFormatter
sfWidgetFormI18nSelectCurrency sfWidgetFormSelect
sfWidgetFormI18nSelectLanguage sfWidgetFormSelectCheckbox
sfWidgetFormI18nTime sfWidgetFormSelectMany
sfWidgetFormInput sfWidgetFormSelectRadio
sfWidgetFormInputCheckbox sfWidgetFormTextarea
sfWidgetFormInputFile sfWidgetFormTime
sfWidgetFormInputFileEditable
$this‐>mergeForm(new OtroForm());
$this‐>embedForm('name', new OtroForm());
Formularios de
Propel
schema.yml
$ ./symfony propel:build‐forms
lib/form/
class JobeetJobForm extends BaseJobeetJobForm
{
public function configure()
{
unset(
$this['created_at'],
$this['updated_at'],
$this['expires_at'],
$this['is_activated']
);
}
}
class JobeetJobForm extends BaseJobeetJobForm
{
public function configure()
{
...
$this‐>validatorSchema['email'] =
new sfValidatorEmail();
}
}
class JobeetJobForm extends BaseJobeetJobForm
{
public function configure()
{
...
$this‐>widgetSchema['type'] =
new sfWidgetFormChoice(array(
'choices' => JobeetJobPeer::$types,
'expanded' => true,
));
} class JobeetJobPeer extends BaseJobeetJobPeer {
true false
false true
true true
class JobeetJobForm extends BaseJobeetJobForm
{
public function configure()
{
...
$this‐>validatorSchema['type'] =
new sfValidatorChoice(array(
'choices' => array_keys(JobeetJobPeer::$types),
));
}
}
class JobeetJobForm extends BaseJobeetJobForm
{
public function configure()
{
...
$this‐>widgetSchema‐>setLabels(array(
'category_id' => 'Category',
'is_public' => 'Public?',
'how_to_apply' => 'How to apply?',
));
}
}
class JobeetJobForm extends BaseJobeetJobForm
{
public function configure()
{
...
$this‐>widgetSchema['logo'] =
new sfWidgetFormInputFile(array(
'label' => 'Company logo',
));
$this‐>validatorSchema['logo'] =
new sfValidatorFile(array(
'required' => false,
'label' => sfConfig::get('sf_upload_dir').'/jobs',
'mime_types' => 'web_images',
));
}
}
sfValidatorFile
1. Valida que el archivo subido sea una
imagen
2. Cambia el nombre del archivo por un
valor único
3. Guarda el archivo en la ruta indicada
4. Actualiza el valor de la columna logo
class JobeetJobForm extends BaseJobeetJobForm
{
public function configure()
{
...
$this‐>widgetSchema‐>setHelp(
'is_public',
'Indica si la oferta de trabajo se puede
publicar en sitios web de afiliados'
);
}
}
apps/frontend/modules/job/templates/ newSuccess.php
<h1>Post a Job</h1>
<?php
include_partial('form', array('form' => $form))
?>
parcial _form
apps/frontend/modules/job/templates/ _form.php
<?php include_stylesheets_for_form($form) ?>
<?php include_javascripts_for_form($form) ?>
method, enctype
<?php echo form_tag_for($form, '@job') ?>
<table id="job_form">
<tfoot><tr><td colspan="2">
<input type="submit" value="Preview job" />
</td></tr></tfoot>
<tbody>
<?php echo $form ?>
</tbody>
</table>
</form>
Formulario Widgets
render() renderRow()
renderHiddenFields() render()
hasErrors() renderLabel()
hasGlobalErrors() renderError()
getGlobalErrors() renderHelp()
renderGlobalErrors()
<?php echo $form ?>
return parent::save($con);
}
lib/form/ JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm {
public function configure() {
unset( $this['token'] );
}
}
apps/frontend/config/ routing.yml
job:
class: sfPropelRouteCollection
options: { model: JobeetJob, column: token }
requirements: { token: \w+ }
http://localhost.jobeet/job/TOKEN/edit
La página de
previsualización
apps/frontend/modules/category/templates/showSuccess.php
<h3>Admin</h3> apps/frontend/modules/job/templates/_admin.php
<ul>
<?php if (!$job‐>getIsActivated()): ?>
<li><?php echo link_to('Edit', 'job_edit', $job) ?></li>
<li><?php echo link_to('Publish', 'job_edit', $job) ?></li>
<?php endif; ?>
...
<?php if ($job‐>isExpired()): ?>
Expired
<?php else: ?>
Expires in <strong>
<?php echo $job‐>getDaysBeforeExpires() ?></strong>
days
<?php endif; ?>
...
Activando y
publicando las
ofertas
apps/frontend/config/ routing.yml
job:
class: sfPropelRouteCollection
options:
model: JobeetJob
column: token
object_actions: { publish: put }
requirements:
token: \w+
apps/frontend/modules/job/actions/actions.class.php
$job = $this‐>getRoute()‐>getObject();
$job‐>publish();
$this‐>getUser()‐>setFlash(
'notice',
sprintf('Your job is now online for %s days.',
sfConfig::get('app_active_days'))
);
$this‐>redirect($this‐>generateUrl('job_show_user', $job));
}
Capítulo 11
Probando los
formularios
Enviando un
formulario
test/functional/frontend/jobActionsTest.php
$browser‐>info('3 ‐ Post a Job page')‐>
info(' 3.1 ‐ Submit a Job')‐>
get('/job/new')‐>
with('request')‐>begin()‐>
isParameter('module', 'job')‐>
isParameter('action', 'new')‐>
end()‐> Preview your job
click('Preview your job', array('job' => array(
'company' => 'Sensio Labs',
'url' => 'http://www.sensio.com/',
'logo' => sfConfig::get('sf_upload_dir').'/jobs/sensio‐labs.gif',
'position' => 'Developer',
'location' => 'Atlanta, USA',
'is_public' => false,
)))‐>
with('request')‐>begin()‐>
isParameter('module', 'job')‐>
isParameter('action', 'create')‐>
end()‐>
;
Seguridad
$ symfony generate:app jobeet ‐‐escaping‐strategy=on ‐‐csrf‐secret=secreto frontend
XSS
<p>Soy un
<p>Soy un usuario
usuario
malvado</p>
malvado</p>
y voy a meter JS
y voy a meter JS
<script
<script
type="text/javascript
type="text/javas
">document.write("Hol
cript">docume
a!")</script>
nt.write("Hola!&
quot;)</script>
$ symfony generate:app jobeet ‐‐escaping‐strategy=on ‐‐csrf‐secret=secreto frontend
CSRF
<form>
<input type="hidden"
<form>
name="_csrf_token"
<input type="text" .../>
value="..." />
<input type="text" .../>
<input type="text" .../>
...
<input type="text" .../>
</form>
...
</form>
Tareas de
mantenimiento
$ php lib/vendor/symfony/data/bin/symfony
lib/task/JobeetCleanupTask.class.php
class JobeetCleanupTask extends sfBaseTask {
protected function configure() {
$this‐>addOptions(array(
new sfCommandOption('env', null, sfCommandOption::PARAMETER_REQUIRED,
'The environement', 'prod'),
new sfCommandOption('days', null, sfCommandOption::PARAMETER_REQUIRED,
'', 90), ));
$this‐>namespace = 'jobeet';
$this‐>name = 'cleanup';
$this‐>briefDescription = 'Cleanup Jobeet database';
$this‐>detailedDescription = <<<EOF
The [jobeet:cleanup|INFO] task cleans up the Jobeet database: [./symfony
jobeet:cleanup ‐‐env=prod ‐‐days=90|INFO]
EOF;
}