Curso Symfony Clase 1

  • Uploaded by: Julio Cesar Brizuela
  • 0
  • 0
  • June 2020
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View Curso Symfony Clase 1 as PDF for free.

More details

  • Words: 2,822
  • Pages: 192
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

<br /> <br /> plantilla<br /> <br /> <title>             

layout

Título de la página

plantilla

apps/frontend/templates/

layout.php

<?php include_slot('title') ?> 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

Related Documents


More Documents from ""

Blender - Tutorial Casa
October 2019 12
December 2019 15
Curso Symfony Clase 2
June 2020 12
Curso Symfony Clase 4
June 2020 12
Blender - Tutorial Ginger
October 2019 20