Frameworks de desarrollo
Symfony Clase 1
Javier Eguíluz
[email protected]
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
The use of these slides in commercial courses or trainings is explicitly prohibited http://creativecommons.org/licenses/by‐nc‐sa/3.0/es/
Capítulo 1
Comenzando el proyecto
¿Qué es Symfony?
Framework para el desarrollo de aplicaciones web con PHP
• El más profesional • El más documentado • El mejor
PHP Frameworks Productividad Calidad programación Mantenimiento Rendimiento aplicación
aprendizaje
Curva de aprendizaje de Symfony
tiempo
¿Qué es Jobeet?
diciembre 2008 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
La mejor forma de aprender Symfony 1.2 a través de 24 tutoriales de 1 hora
Un tutorial diferente
• Serio • Profesional • Completo
Prerrequisitos
5.2.4
Instalación de Symfony
http://www.symfony‐project.org/installation/1_2
symfony‐1.2.4.tgz
check_configuration.php
$ php lib/vendor/symfony/data/bin/symfony
Preparar el proyecto
frontend
backend
proyecto aplicación
jobeet
frontend
backend
$ symfony generate:project jobeet
apps/
log/
cache/
plugins/
config/
test/
lib/
web/
$ symfony generate:app jobeet ‐‐escaping‐strategy=on ‐‐csrf‐secret=UniqueSecret
frontend
frontend/ config/ lib/ modules/ templates/
‐‐escaping‐strategy
‐‐csrf‐secret
XSS CSRF
config/ProjectConfiguration.class.php
require_once dirname(__FILE__). '/../lib/vendor/'. 'symfony/lib/autoload/'. 'sfCoreAutoload.class.php';
Los entornos
• Entorno de desarrollo (dev) • Entorno de pruebas • Entorno intermedio • Entorno de producción (prod)
Errores en el entorno de desarrollo (dev)
Errores en el entorno de producción (prod)
web/index.php
'frontend', 'prod', false ); sfContext::createInstance($configuration)‐>dispatch();
Configurar bien el servidor web
ServerName jobeet.localhost DocumentRoot "/home/sfprojects/jobeet/web" DirectoryIndex index.php AllowOverride All Allow from All Alias /sf /home/sfprojects/jobeet/lib/vendor/symfony/data/web/sf AllowOverride All Allow from All
/etc/hosts c:\windows\system32\drivers\etc\hosts
127.0.0.1 jobeet.localhost
http://jobeet.localhost/
prod
http://jobeet.localhost/frontend_dev.php
dev
Versionado de código
Capítulo 2
El proyecto
La idea del proyecto
Aplicación de software libre que permite crear sitios web de búsqueda de empleo
características • Completo y personalizable • Multilingüe • AJAX, RSS y API
Los escenarios del proyecto
• administrador (admin) • usuario (user) • publicador (poster) • afiliado (affiliate)
F1 El usuario accede a la portada y ve las últimas ofertas de trabajo activas
F2 El usuario puede visualizar todas las ofertas de trabajo de una categoría
F3 El usuario refina el listado mediante palabras clave
F4 El usuario pincha sobre una oferta de trabajo para ver más información
F5 El usuario publica una nueva oferta de trabajo
F6 El usuario quiere convertirse en un afiliado
F7 Un usuario afiliado obtiene la lista de ofertas de trabajo activas
B1 El administrador configura el sitio web
B2 El administrador gestiona las ofertas de trabajo
B3 El administrador gestiona los afiliados
Capítulo 3
El modelo de datos
El modelo relacional
relacional
ORM
objetos
1. Describir la base de datos 2. Generar las clases PHP 3. Trabajar con objetos en vez
de SQL
El formato YAML
YAML Formato para serializar datos que es fácil de leer por las personas y es compatible con todos los lenguajes de programación
$casa = array( 'familia' => array( 'apellido' => 'García', 'padres' => array('Antonio', 'María'), 'hijos' => array('Jose', 'Manuel') ), 'direccion' => array( 'numero' => 34, 'calle' => 'Gran Vía', 'ciudad' => 'Cualquiera', 'codigopostal' => '12345' ) );
casa: familia: apellido: García padres: ‐ Antonio ‐ María hijos: ‐ Jose ‐ Manuel direccion: numero: 34 calle: Gran Vía ciudad: Cualquiera codigopostal: "12345"
casa: familia: { apellido: García, padres: [Antonio, María], hijos: [Jose, Manuel] } direccion: { numero: 34, direccion: Gran Vía, ciudad: Cualquiera, codigopostal: "12345" }
Sintaxis clave: clave: valor clave: clave: valor clave: ‐ valor ‐ valor
clave: clave: valor 2 espacios en blanco
Arrays normales clave: ‐ valor1 ‐ valor2 ‐ valor3 clave:[valor1, valor2, valor3]
Arrays asociativos clave: clave1: valor1 clave2: valor2 clave3: valor3 clave: { clave1: valor1, clave2: valor2, clave3: valor3}
XML
<matches> <match> 2002‐10‐04 <White refid="fritz" /> Draw <match> 2002‐10‐06 <White refid="kramnik" /> White
YAML players: Vladimir Kramnik: &kramnik rating: 2700 status: GM Deep Fritz: &fritz rating: 2700 status: Computer David Mertz: &mertz rating: 1400 status: Amateur matches: ‐ Date: 2002‐10‐04 White: *fritz Black: *kramnik Result: Draw ‐ Date: 2002‐10‐06 White: *kramnik Black: *fritz Result: White
El esquema
config/
schema.yml
propel: jobeet_category: id: ~ name: { type: varchar(255), required: true, index: unique } jobeet_job: id: ~ category_id: { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true } type: { type: varchar(255) } company: { type: varchar(255), required: true } logo: { type: varchar(255) } url: { type: varchar(255) } position: { type: varchar(255), required: true } location: { type: varchar(255), required: true } description: { type: longvarchar, required: true } how_to_apply: { type: longvarchar, required: true } token: { type: varchar(255), required: true, index: unique } is_public: { type: boolean, required: true, default: 1 } is_activated: { type: boolean, required: true, default: 0 } email: { type: varchar(255), required: true } expires_at: { type: timestamp, required: true } created_at: ~ updated_at: ~ jobeet_affiliate: id: ~ url: { type: varchar(255), required: true } email: { type: varchar(255), required: true, index: unique } token: { type: varchar(255), required: true } is_active: { type: boolean, required: true, default: 0 } created_at: ~ jobeet_category_affiliate: category_id: { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true, primaryKey: true, onDelete: cascade } affiliate_id: { type: integer, foreignTable: jobeet_affiliate, foreignReference: id, required: true, primaryKey: true, onDelete: cascade }
config/
schema.yml
propel: jobeet_category: id: ~ name: { type: varchar(255), required: true, index: unique }
config/
schema.yml
jobeet_job: id: ~ category_id: { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true } type: { type: varchar(255) } company: { type: varchar(255), required: true } logo: { type: varchar(255) } url: { type: varchar(255) } position: { type: varchar(255), required: true } location: { type: varchar(255), required: true } description: { type: longvarchar, required: true } how_to_apply: { type: longvarchar, required: true } token: { type: varchar(255), required: true, index: unique } is_public: { type: boolean, required: true, default: 1 } is_activated: { type: boolean, required: true, default: 0 } email: { type: varchar(255), required: true } expires_at: { type: timestamp, required: true } created_at: ~ updated_at: ~
config/
schema.yml
jobeet_affiliate: id: ~ url: { type: varchar(255), required: true } email: { type: varchar(255), required: true, index: unique } token: { type: varchar(255), required: true } is_active: { type: boolean, required: true, default: 0 } created_at: ~
config/ jobeet_category_affiliate: category_id: { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true, primaryKey: true, onDelete: cascade } affiliate_id: { type: integer, foreignTable: jobeet_affiliate, foreignReference: id, required: true, primaryKey: true, onDelete: cascade }
schema.yml
config/
•
schema.yml
type: boolean, tinyint, smallint, integer, bigint, double, float, real, decimal, char, varchar(size), longvarchar, date, time, timestamp, blob, clob
•
required: true, false
•
index: true, false
•
primaryKey: true, false
•
foreignKey, foreignReference
La base de datos
$ mysqladmin ‐uroot ‐p create jobeet $ symfony configure:database “mysql:host=localhost;dbname=jobeet” root ConTraSenA config/databases.yml
El ORM
$ symfony propel:build‐sql data/sql/ $ symfony propel:insert‐sql ‐‐no‐confirmation $ symfony propel:build‐model lib/model/
• • • •
extends
JobeetJob BaseJobeetJob JobeetJobPeer BaseJobeetJobPeer
extends
$job = new JobeetJob(); $job‐>setPosition('Web developer'); $job‐>save(); echo $job‐>getPosition(); $job‐>delete();
$category = new JobeetCategory(); $category‐>setName('Programming'); $job = new JobeetJob(); $job‐>setCategory($category);
$ symfony propel:build‐all ‐‐no‐confirmation
+
$ symfony propel:build‐sql $ symfony propel:insert‐sql $ symfony propel:build‐model $ symfony propel:build‐forms $ symfony propel:build‐filters
$ symfony propel:build‐all
$ symfony cc Borra la caché de Symfony • Ejecutar siempre que añades clases (autoload) • La solución de casi todos los errores de los principiantes •
$ symfony cache:clear $ symfony cache:cl $ symfony ca:c $ symfony cc
Los datos iniciales
data/fixtures/
• • •
Datos iniciales Datos de prueba Datos de usuarios
data/fixtures/
010_categories.yml
JobeetCategory: design: { name: Design } programming: { name: Programming } manager: { name: Manager } administrator: { name: Administrator }
data/fixtures/
020_jobs.yml
JobeetJob: job_sensio_labs: category_id: programming type: full‐time company: Sensio Labs logo: sensio‐labs.gif url: http://www.sensiolabs.com/ position: Web Developer location: Paris, France description: | You have already developed websites with symfony and you want to work with Open‐Source technologies. You have a minimum of 3 years experience in web development with PHP or Java and you wish to participate to development of Web 2.0 sites using the best frameworks available. how_to_apply: | Send your resume to fabien.potencier [at] sensio.com is_public: true is_activated: true token: job_sensio_labs email:
[email protected] expires_at: 2010‐10‐10
$ symfony propel:data‐load
+
$ symfony propel:build‐sql $ symfony propel:insert‐sql $ symfony propel:build‐model $ symfony propel:build‐forms $ symfony propel:build‐filters $ symfony propel:data‐load
$ symfony propel:build‐all‐load
Probando la aplicación en el navegador
proyecto aplicación módulo
jobeet
frontend
job
backend
$ symfony propel:generate‐module ‐‐with‐show ‐‐non‐verbose‐templates
frontend job JobeetJob
frontend/modules/job actions/ templates/
frontend/modules/job/actions/actions.class.php
index
edit
show
update
new
delete
create
http://jobeet.localhost/frontend_dev.php/job
frontend _dev job
Objeto
Categoría
Representación textual
_ _toString()
lib/model/
JobeetCategory.php
class JobeetCategory extends BaseJobeetCategory { public function __toString() { return $this‐>getName(); } }
lib/model/
class JobeetJob extends BaseJobeetJob { public function __toString() { return sprintf( '%s at %s (%s)', $this‐>getPosition(), $this‐>getCompany(), $this‐>getLocation() ); } }
JobeetJob.php
lib/model/
JobeetAffiliate.php
class JobeetAffiliate extends BaseJobeetAffiliate { public function __toString() { return $this‐>getUrl(); } }
http://jobeet.localhost/frontend_dev.php/job
Capítulo 4
El controlador y la vista
La arquitectura MVC
¿Cómo se programaba con PHP en el siglo pasado?
1 página del sitio web
=
1 archivo PHP diferente
¿Cómo se programaba con PHP en el siglo pasado? inicialización y configuración lógica de negocio acceso a BBDD generar código HTML pagina.php
Modelo Vista Controlador
Modelo Directorio /lib/model Vista Directorios templates/ Controlador Archivos index.php y frontend_dev.php Archivos actions.class.php
El layout
patrón de diseño decorator
apps/frontend/templates/layout.php
apps/frontend/templates/
layout.php
Jobeet ‐ Your best job board ...
Plantillas Symfony • Archivos PHP
normales • Existe un plugin
para Smarty • Symfony 2.0 podría
incluir plantillas
Las hojas de estilos, imágenes y archivos JavaScript
helpers ...
apps/frontend/config/ default: http_metas: content‐type: text/html metas: #title: symfony project #description: symfony project #keywords: symfony, project #language: en #robots: index, follow stylesheets: [main.css] javascripts: [] has_layout: on layout: layout
view.yml
apps/frontend/config/
view.yml
default: ...
stylesheets: [main.css, jobs.css, job.css] ...
apps/frontend/config/
view.yml
default: ...
stylesheets: [main.css, jobs.css, job] ...
apps/frontend/config/
view.yml
default: ...
stylesheets: [main.css, /css/v2/jobs.css] ...
apps/frontend/config/
view.yml
default: ...
stylesheets: [main.css, jobs: { media: print } ] ...
metas: title: El título 1
Symfony
metas: title: El título 2
Proyecto
metas: title: El título 3
Aplicación
metas: title: El título 4
Módulo
view.yml
Symfony lib/vendor/symfony/lib/config/config/view.yml Proyecto config/view.yml Aplicación apps/frontend/config/view.yml Módulo apps/frontend/modules/job/config/view.yml
metas: stylesheets: [job]
view.yml
plantilla plantilla
La portada del módulo de las ofertas de trabajo
apps/ frontend/ modules/ job/ actions/ actions.class.php templates/ indexSuccess.php
acción
=
+ plantilla
http://jobeet.localhost/frontend_dev.php/job/index
frontend (aplicación) _dev (entorno) job (módulo) index (acción executeIndex y plantilla indexSuccess)
apps/frontend/modules/job/actions/
actions.class.php
class jobActions extends sfActions { public function executeIndex(sfWebRequest $request) { $this‐>jobeet_job_list = JobeetJobPeer::doSelect(new Criteria()); } // ... }
SELECT [ALL | DISTINCT | DISTINCTROW ] [HIGH_PRIORITY] [STRAIGHT_JOIN] [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT] [SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS] select_expr [, select_expr ...] [FROM table_references [WHERE where_condition] [GROUP BY {col_name | expr | position} [ASC | DESC], ... [WITH ROLLUP]] [HAVING where_condition] [ORDER BY {col_name | expr | position} [ASC | DESC], ...] [LIMIT {[offset,] row_count | row_count OFFSET offset}] [PROCEDURE procedure_name(argument_list)] [INTO OUTFILE 'file_name' export_options | INTO DUMPFILE 'file_name' | INTO var_name [, var_name]] [FOR UPDATE | LOCK IN SHARE MODE]]
apps/frontend/modules/job/templates/
indexSuccess.php
...
getId() ?> ...
La plantilla de la página de una oferta de trabajo
Slots
Jobeet
layout
plantilla
layout
Título de la página
plantilla
apps/frontend/templates/
layout.php
apps/frontend/modules/job/templates/
indexSuccess.php
getCompany(), $job‐>getPosition() ) ) ?>
La acción de la página de una oferta de trabajo
apps/frontend/modules/job/actions/
actions.class.php
public function executeShow(sfWebRequest $request) { $this‐>job = JobeetJobPeer::retrieveByPk( $request‐>getParameter('id') ); $this‐>forward404Unless($this‐>job); }
La petición y la respuesta
objeto sfWebRequest class jobActions extends sfActions { public function executeShow(sfWebRequest { $peticion = $request; $origen = $peticion‐>getReferer(); $metodo = $peticion‐>getMethod(); } // ... }
$request)
Nombre del método
Equivalente de PHP
getMethod()
$_SERVER['REQUEST_METHOD']
getUri()
$_SERVER['REQUEST_URI']
getReferer()
$_SERVER['HTTP_REFERER']
getHost()
$_SERVER['HTTP_HOST']
getLanguages()
$_SERVER['HTTP_ACCEPT_LANGUAGE']
getCharsets()
$_SERVER['HTTP_ACCEPT_CHARSET']
isXmlHttpRequest()
$_SERVER['X_REQUESTD_WITH'] == 'XMLHttpRequest'
getHttpHeader()
$_SERVER
getCookie()
$_COOKIE
isSecure()
$_SERVER['HTTPS']
getFiles()
$_FILES
getGetParameter()
$_GET
getPostParameter()
$_POST
getUrlParameter()
$_SERVER['PATH_INFO']
getRemoteAddress()
$_SERVER['REMOTE_ADDR']
public function executeShow(sfWebRequest $request) { ... $request‐>getParameter('id'); ... }
/ruta1/ruta2/ruta3?id=3&clave1= valor1&clave2=valor2
objeto sfWebResponse class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $respuesta = $this‐>getResponse(); $respuesta‐>setStatusCode(404); $respuesta‐>addStyleSheet('/css/job.css'); $respuesta‐>setTitle('Título de la página'); } // ... }
archivo de configuración metas: stylesheets: [job]
plantilla
use_stylesheet('job.css')
?>
acción $this‐>getResponse()‐> addStyleSheet('/css/job.css');
Capítulo 5
El sistema de enrutamiento
URL
internet
symfony
URL
URI
sistema de enrutamiento
URI 'job/show?id='.$job‐>getId()
url_for() URL
job/show/id/1
URI modulo/accion?clave1= valor1&clave2=valor2& ...
Configurando el enrutamiento
apps/frontend/config/
routing.yml
homepage: url: / param: { module: default, action: index } default_index: url: /:module param: { action: index } default: url: /:module/:action/*
nombre patrón default_index: url: /:module param: { action: index } parámetros
homepage: url: / param: { module: default, action: index } default_index: url: /:module param: { action: index } default: url: /:module/:action/*
/job
/frontend_dev.php/job
1
entorno
aplicación
¿módulo? 2
???
¿acción?
apps/frontend/config/routing.yml
/job
default_index: url: /:module param: { action: index }
:module = job = módulo
3
apps/frontend/config/routing.yml
default_index: url: /:module param: { action: index }
4
acción = index
/frontend_dev.php/job
aplicación = frontend entorno = dev
módulo = job acción = index
URI
url_for()
URL
url_for('job/show?id='.$job‐>getId()) /job/show/id/1
URI
url_for()
URL
url_for('@default?id='.$job‐>getId())
Personalizando el enrutamiento
apps/frontend/config/
routing.yml
homepage: url: / param: { module: job, action: index } default_index: url: /:module param: { action: index } default: url: /:module/:action/*
/job/sensio‐labs/paris‐france/1/web‐developer 1. Identificar el patrón de la URL 2. Incluir la ruta a routing.yml 3. Añadir las restricciones adecuadas
/job/sensio‐labs/paris‐france/1/web‐developer 1. Identificar el patrón de la URL
/job/:company/:location/:id/:position
/job/sensio‐labs/paris‐france/1/web‐developer 2. Incluir la ruta en routing.yml job_show_user: url: /job/:company/:location/:id/:position param: { module: job, action: show }
url_for('job/show?'. 'id='.$job‐>getId(). '&company='.$job‐>getCompany(). '&location='.$job‐>getLocation(). '&position='.$job‐>getPosition() )
url_for(array( 'module' => 'job', 'action' => 'show', 'id' => $job‐>getId(), 'company' => $job‐>getCompany(), 'location' => $job‐>getLocation(), 'position' => $job‐>getPosition(), ))
Requisitos
/job/sensio‐labs/paris‐france/1/web‐developer 3. Añadir las restricciones adecuadas job_show_user: url: /job/:company/:location/:id/:position param: { module: job, action: show } requirements: id: \d+
La clase sfRoute
HTTP Navegadores Symfony GET POST HEAD PUT DELETE
job_show_user: url: /job/:company/:location/:id/:position
class: sfRequestRoute param: { module: job, action: show } requirements: id: \d+
sf_method: [get]
La clase para las rutas basadas en objetos
url_for('job/show?'. 'id='.$job‐>getId(). '&company='.$job‐>getCompany(). '&location='.$job‐>getLocation(). '&position='.$job‐>getPosition() )
job_show_user: url: /job/:company/:location/:id/:position
class: sfPropelRoute options: model: JobeetJob type: object param: { module: job, action: show } requirements: id: \d+ sf_method: [get]
url_for('job/show?'. 'id='.$job‐>getId(). '&company='.$job‐>getCompany(). '&location='.$job‐>getLocation(). '&position='.$job‐>getPosition() )
url_for('job_show_user', $job) url_for(array( 'sf_route' => 'job_show_user', 'sf_subject' => $job ))
Lo que queremos… /job/sensio‐labs/paris‐france/1/web‐developer
Lo que tenemos… /job/Sensio+Labs/Paris%2C+France/1/Web+Developer
getId() id name description ...
getName()
Job
getDescription() getSlug()
schema.yml
getShortDescription()
getters virtuales
slug Comienza el curso de Symfony en Vitoria‐Gasteiz www.symfony.es/2009/01/29/comienza‐el‐curso‐de‐symfony‐en‐vitoria‐gasteiz
Twitto, el framework PHP más pequeño www.symfony.es/2009/01/11/twitto‐el‐framework‐php‐mas‐pequeno/
getId() id name description ...
getName()
Job
getDescription() getCompanySlug() getPositionSlug()
schema.yml
getters virtuales
getLocationSlug()
lib/model/
JobeetJob.php
public function getCompanySlug() { return Jobeet::slugify($this‐>getCompany()); } public function getPositionSlug() { return Jobeet::slugify($this‐>getPosition()); } public function getLocationSlug() { return Jobeet::slugify($this‐>getLocation()); }
lib/
Jobeet.class.php
class Jobeet { static public function slugify($text) { // replace all non letters or digits by ‐ $text = preg_replace('/\W+/', '‐', $text); // trim and lowercase $text = strtolower(trim($text, '‐')); return $text; } }
job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: show } requirements: id: \d+ sf_method: [get]
class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this‐>job = JobeetJobPeer::retrieveByPk($request‐>getParameter('id')); $this‐>forward404Unless($this‐>job); } // ... }
class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this‐>job = $this‐>getRoute()‐>getObject(); } // ... }
Enrutamiento en acciones y plantillas
"> Texto del enlace
class jobActions extends sfActions {
public function executeIndex(sfWebRequest
$request)
{ // ... $this‐>redirect( $this‐>generateUrl('job_show_user', $job) ); } // ... }
La clase de las colecciones de rutas
job: class: sfPropelRouteCollection options: { model: JobeetJob } job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: show } requirements: id: \d+ sf_method: [get]
$ php symfony app:routes frontend