115
Symfony en Drupal 8 Raul Fraile

Symfony en Drupal 8 - DrupalCamp Spain

Embed Size (px)

DESCRIPTION

Presentación sobre los componentes de Symfony utilizados en Drupal 8. DrupalCamp Spain

Citation preview

Page 1: Symfony en Drupal 8 - DrupalCamp Spain

Symfony en Drupal 8 Raul Fraile

Page 2: Symfony en Drupal 8 - DrupalCamp Spain

Raúl Fraile (@raulfraile)

PHP/Symfony2 dev @

PHP 5.3 Zend Certified Engineer

Symfony Certified Developer

LadybugPHP

Sobre mi

Page 3: Symfony en Drupal 8 - DrupalCamp Spain

Drupal Island

Page 4: Symfony en Drupal 8 - DrupalCamp Spain

Drupal Island

Durante años, Drupal ha sido la envidia de muchos CMS/frameworks: Comunidad activa, robustez, extensibilidad…

Page 5: Symfony en Drupal 8 - DrupalCamp Spain

Drupal Island

https://www.flickr.com/photos/peterlozano/14020046267/

Page 6: Symfony en Drupal 8 - DrupalCamp Spain

Drupal Island

En los últimos años, PHP ha mejorado en muchos aspectos, y no sólo como lenguaje.

Ésta mejora “obliga” a Drupal a cambiar para dirigirse a un mercado más empresarial/profesional.

Page 7: Symfony en Drupal 8 - DrupalCamp Spain

Drupal Island

5.3+

Mejoras en OO

Namespaces

Closures

Traits

Funciones generadoras

Composer

Page 8: Symfony en Drupal 8 - DrupalCamp Spain

function drupal_http_request($url, array $options = array()) { // Allow an alternate HTTP client library to replace Drupal's default // implementation. $override_function = variable_get('drupal_http_request_function', FALSE); if (!empty($override_function) && function_exists($override_function)) { return $override_function($url, $options); } ! $result = new stdClass(); ! // Parse the URL and make sure we can handle the schema. $uri = @parse_url($url); ! if ($uri == FALSE) { $result->error = 'unable to parse URL'; $result->code = -1001; return $result; } ! if (!isset($uri['scheme'])) { $result->error = 'missing schema'; $result->code = -1002; return $result; } ! timer_start(__FUNCTION__); ! // Merge the default options. $options += array( 'headers' => array(), 'method' => 'GET',

Drupal Islanddrupal_http_request()

Page 9: Symfony en Drupal 8 - DrupalCamp Spain

} break; default: $result->error = $status_message; } ! return $result; }

Drupal Islanddrupal_http_request()

Page 10: Symfony en Drupal 8 - DrupalCamp Spain

} break; default: $result->error = $status_message; } ! return $result; }

Drupal Island

LOC: 304

Complejidad ciclomática: 41

N-Path: 25.303.344.960

drupal_http_request()

Page 11: Symfony en Drupal 8 - DrupalCamp Spain

Drupal Island

N-Path ≈ Núm. caminos ≈ Tests

2 TB de tests

412 DVDs de tests

670K Drupals de tests

drupal_http_request()

Page 12: Symfony en Drupal 8 - DrupalCamp Spain

Drupal Island

slideshare.net/ircmaxell/development-by-the-numbers

Anthony Ferrara

Page 13: Symfony en Drupal 8 - DrupalCamp Spain

Drupal Island

Dificultad para mantener el código.

Código antiguo, compatibilidad con PHP 4.

Orientación a objetos testimonial.

Reinventando la rueda.

Page 14: Symfony en Drupal 8 - DrupalCamp Spain

Drupal Island

Page 15: Symfony en Drupal 8 - DrupalCamp Spain

Drupal Island

Page 16: Symfony en Drupal 8 - DrupalCamp Spain

Drupal Island

Page 17: Symfony en Drupal 8 - DrupalCamp Spain

Drupal Island

Page 18: Symfony en Drupal 8 - DrupalCamp Spain

NIHNot Invented Here

Drupal Island

Page 19: Symfony en Drupal 8 - DrupalCamp Spain

NIHNot Invented Here

PIEProudly Found Elsewhere

Drupal Island

Page 20: Symfony en Drupal 8 - DrupalCamp Spain

ClassLoader DependencyInjection

EventDispatcher HttpFoundation

HttpKernel Routing Serializer Validator

Yaml

Twig Doctrine Common

Doctrine Annotations Guzzle Assetic

SymfonyCMF Routing EasyRDF PHPUnit

Zend Feed

Drupal Island

Page 21: Symfony en Drupal 8 - DrupalCamp Spain

50% de las dependencias de

Drupal 8 son componentes de

Symfony

Drupal Island

Page 22: Symfony en Drupal 8 - DrupalCamp Spain

¿Por qué Symfony?

Page 23: Symfony en Drupal 8 - DrupalCamp Spain

Proyecto maduro y de calidad.

Basado en componentes. “Líder” de la revolución contra los frameworks monolíticos.

Comunidad grande y activa.

¿Por qué Symfony?

Page 24: Symfony en Drupal 8 - DrupalCamp Spain

Componentes de Symfony2

Page 25: Symfony en Drupal 8 - DrupalCamp Spain

Conjunto de librerías desacopladas e independientes.

Implementan funcionalidad común para sitios/apps web.

Bloques con los que se construye el full-stack framework.

Componentes de Symfony2

Page 26: Symfony en Drupal 8 - DrupalCamp Spain
Page 27: Symfony en Drupal 8 - DrupalCamp Spain

HttpFoundation

Page 28: Symfony en Drupal 8 - DrupalCamp Spain

Abstracción del protocolo HTTP.

El origen de la colaboración entre

Symfony y Drupal.

HttpFoundation

Page 29: Symfony en Drupal 8 - DrupalCamp Spain

HttpFoundation GET /index.php HTTP/1.1 Host: test.com Accept-Language:en;q=0.8 Accept-Encoding:gzip User-Agent: Mozilla/5.0

Page 30: Symfony en Drupal 8 - DrupalCamp Spain

HttpFoundation

$_GET $_POST $_COOKIE $_FILES $_SERVER

GET /index.php HTTP/1.1 Host: test.com Accept-Language:en;q=0.8 Accept-Encoding:gzip User-Agent: Mozilla/5.0

Page 31: Symfony en Drupal 8 - DrupalCamp Spain

query request cookies files server headers

getScheme getHost

getClientIp getMethod

getContentType getPreferredLanguage

HttpFoundation

$_GET $_POST $_COOKIE $_FILES $_SERVER

GET /index.php HTTP/1.1 Host: test.com Accept-Language:en;q=0.8 Accept-Encoding:gzip User-Agent: Mozilla/5.0

Page 32: Symfony en Drupal 8 - DrupalCamp Spain

HttpFoundation

use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; !$request = Request::createFromGlobals(); !$html = sprintf( '<h1>Hola %s!</h1>', $request->query->get('name', 'Raul')); !$response = new Response($html); $response ->headers->set("content-type", "text/html"); $response->send();

Page 33: Symfony en Drupal 8 - DrupalCamp Spain

HttpFoundation

use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; !$request = Request::createFromGlobals(); !$html = sprintf( '<h1>Hola %s!</h1>', $request->query->get('name', 'Raul')); !$response = new Response($html); $response ->headers->set("content-type", "text/html"); $response->send();

Page 34: Symfony en Drupal 8 - DrupalCamp Spain

HttpFoundation

use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; !$request = Request::createFromGlobals(); !$html = sprintf( '<h1>Hola %s!</h1>', $request->query->get('name', 'Raul')); !$response = new Response($html); $response ->headers->set("content-type", "text/html"); $response->send();

Page 35: Symfony en Drupal 8 - DrupalCamp Spain

HttpFoundation

use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; !$request = Request::createFromGlobals(); !$html = sprintf( '<h1>Hola %s!</h1>', $request->query->get('name', 'Raul')); !$response = new Response($html); $response ->headers->set("content-type", "text/html"); $response->send();

Page 36: Symfony en Drupal 8 - DrupalCamp Spain
Page 37: Symfony en Drupal 8 - DrupalCamp Spain

HttpKernel

Page 38: Symfony en Drupal 8 - DrupalCamp Spain

El componente HttpKernel define

un proceso abstracto para convertir

un objeto Request en un Response:

HttpKernelInterface

HttpKernel

Page 39: Symfony en Drupal 8 - DrupalCamp Spain

HttpKernel

HttpKernelInterface

Page 40: Symfony en Drupal 8 - DrupalCamp Spain

HttpKernel

Request HttpKernelInterface

Page 41: Symfony en Drupal 8 - DrupalCamp Spain

HttpKernel

Request ResponseHttpKernelInterface

Page 42: Symfony en Drupal 8 - DrupalCamp Spain

HttpKernel

Request ResponseHttpKernelInterface

Page 43: Symfony en Drupal 8 - DrupalCamp Spain

HttpKernel

Request ResponseHttpKernelInterface

Page 44: Symfony en Drupal 8 - DrupalCamp Spain

HttpKernel

Request ResponseHttpKernelInterface

Page 45: Symfony en Drupal 8 - DrupalCamp Spain

HttpKernel

Request ResponseHttpKernelInterface

Page 46: Symfony en Drupal 8 - DrupalCamp Spain

HttpKernel

Aplicación

Page 47: Symfony en Drupal 8 - DrupalCamp Spain

Caché

HttpKernel

Aplicación

Page 48: Symfony en Drupal 8 - DrupalCamp Spain

Negociación

Caché

HttpKernel

Aplicación

Page 49: Symfony en Drupal 8 - DrupalCamp Spain

Negociación

Caché

HttpKernel

Aplicación

Middleware

Page 50: Symfony en Drupal 8 - DrupalCamp Spain

El componente dispone de una implementación concreta de HttpKernelInterface.

Diseñada para ser muy flexible, con eventos “estándar”.

HttpKernel

Page 51: Symfony en Drupal 8 - DrupalCamp Spain
Page 52: Symfony en Drupal 8 - DrupalCamp Spain

ClassLoader

Page 53: Symfony en Drupal 8 - DrupalCamp Spain

El componente ClassLoader permite realizar autoload de clases en PHP.

Un único require/include por aplicación.

Permite cachear las rutas para ganar rendimiento.

Dispone de 2 autoloaders: PSR-0 y MapClass.

ClassLoader

Page 54: Symfony en Drupal 8 - DrupalCamp Spain

Cada vez que se utiliza una clase que no ha sido incluida previamente, PHP utiliza el mecanismo de autoload.

FQN + reglas/map = require(archivo)

ClassLoader

Page 55: Symfony en Drupal 8 - DrupalCamp Spain

ClassLoader

new MyClass()

ClassMap PSR-0/4

Page 56: Symfony en Drupal 8 - DrupalCamp Spain

ClassLoader

new MyClass()

ClassMap PSR-0/4

Page 57: Symfony en Drupal 8 - DrupalCamp Spain

ClassLoader

new MyClass()

ClassMap PSR-0/4

Path

Page 58: Symfony en Drupal 8 - DrupalCamp Spain

require_once( )

ClassLoader

new MyClass()

ClassMap PSR-0/4

Path

Page 59: Symfony en Drupal 8 - DrupalCamp Spain

ClassLoader

new MyClass()

ClassMap PSR-0/4

Page 60: Symfony en Drupal 8 - DrupalCamp Spain

require_once( )

ClassLoader

new MyClass()

ClassMap PSR-0/4

Path

Page 61: Symfony en Drupal 8 - DrupalCamp Spain

require_once( )

ClassLoader

new MyClass()

ClassMap PSR-0/4

Path

Page 62: Symfony en Drupal 8 - DrupalCamp Spain

ClassLoaderPHP Framework Interop Group

The idea behind the group is for project r e p re s en t a t i v e s t o t a l k a bou t t h e commonalities between our projects and find ways we can work together. Our main audience is each other, but we’re very aware that the rest of the PHP community is watching. If other folks want to adopt what we’re doing they are welcome to do so, but that is not the aim.

Page 63: Symfony en Drupal 8 - DrupalCamp Spain

ClassLoaderPHP Framework Interop Group

PSR-0 Autoloading Standard

PSR-1 Basic Coding Standard

PSR-2 Coding Style Guide

PSR-3 Logger Interface

PSR-4 Improved Autoloading

Page 64: Symfony en Drupal 8 - DrupalCamp Spain
Page 65: Symfony en Drupal 8 - DrupalCamp Spain

Routing

Page 66: Symfony en Drupal 8 - DrupalCamp Spain

RoutingCMF

Page 67: Symfony en Drupal 8 - DrupalCamp Spain

El componente Symfony/Routing relaciona peticiones HTTP con un conjunto de variables.

CMF/Routing lo extiende para permitir rutas dinámicas:

URLs definidas por usuarios

Multiidioma

Routing

Page 68: Symfony en Drupal 8 - DrupalCamp Spain

Routing

Request

Controller

ChainRouter

Page 69: Symfony en Drupal 8 - DrupalCamp Spain

Routing

Request

Controller

ChainRouter

R1 R2 R3

R4 R5 R6

Page 70: Symfony en Drupal 8 - DrupalCamp Spain

Routing

Request

Controller

ChainRouter

R1 R2 R3

R4 R5 R6

RouterInterface

Page 71: Symfony en Drupal 8 - DrupalCamp Spain

Routing

Request

Controller

ChainRouter

R1 R2 R3

R4 R5 R6

RouterInterface

Page 72: Symfony en Drupal 8 - DrupalCamp Spain
Page 73: Symfony en Drupal 8 - DrupalCamp Spain

EventDispatcher

Page 74: Symfony en Drupal 8 - DrupalCamp Spain

El componente EventDispatcher implementa el patrón Mediador (Mediator Pattern), permitiendo desacoplar nuestro código.

Alternativa OO a los clásicos hooks de Drupal.

EventDispatcher

Page 75: Symfony en Drupal 8 - DrupalCamp Spain

EventDispatcher

Productor

Consumidor

Consumidor

Consumidor

Consumidor

Med

iado

r

Page 76: Symfony en Drupal 8 - DrupalCamp Spain

EventDispatcher

Productor

Consumidor

Consumidor

Consumidor

Consumidor

Med

iado

r

Page 77: Symfony en Drupal 8 - DrupalCamp Spain

EventDispatcheruse Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\Event; $dispatcher = new EventDispatcher(); // add listeners $dispatcher->addListener('blog.post.saved', function (BlogPostEvent $event) { echo 'Updating RSS feed' . PHP_EOL; }); $dispatcher->addListener('blog.post.saved', function (BlogPostEvent $event) { echo 'Sending emails' . PHP_EOL; }); // save the post // … !// dispatch the event $event = new BlogPostEvent($blogPost); $dispatcher->dispatch('blog.post.saved', $event);

Page 78: Symfony en Drupal 8 - DrupalCamp Spain

EventDispatcheruse Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\Event; $dispatcher = new EventDispatcher(); // add listeners $dispatcher->addListener('blog.post.saved', function (BlogPostEvent $event) { echo 'Updating RSS feed' . PHP_EOL; }); $dispatcher->addListener('blog.post.saved', function (BlogPostEvent $event) { echo 'Sending emails' . PHP_EOL; }); // save the post // … !// dispatch the event $event = new BlogPostEvent($blogPost); $dispatcher->dispatch('blog.post.saved', $event);

Page 79: Symfony en Drupal 8 - DrupalCamp Spain

EventDispatcheruse Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\Event; $dispatcher = new EventDispatcher(); // add listeners $dispatcher->addListener('blog.post.saved', function (BlogPostEvent $event) { echo 'Updating RSS feed' . PHP_EOL; }); $dispatcher->addListener('blog.post.saved', function (BlogPostEvent $event) { echo 'Sending emails' . PHP_EOL; }); // save the post // … !// dispatch the event $event = new BlogPostEvent($blogPost); $dispatcher->dispatch('blog.post.saved', $event);

Page 80: Symfony en Drupal 8 - DrupalCamp Spain

EventDispatcheruse Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\Event; $dispatcher = new EventDispatcher(); // add listeners $dispatcher->addListener('blog.post.saved', function (BlogPostEvent $event) { echo 'Updating RSS feed' . PHP_EOL; }); $dispatcher->addListener('blog.post.saved', function (BlogPostEvent $event) { echo 'Sending emails' . PHP_EOL; }); // save the post // … !// dispatch the event $event = new BlogPostEvent($blogPost); $dispatcher->dispatch('blog.post.saved', $event);

Page 81: Symfony en Drupal 8 - DrupalCamp Spain
Page 82: Symfony en Drupal 8 - DrupalCamp Spain

DependencyInjection

Page 83: Symfony en Drupal 8 - DrupalCamp Spain

El componente DependencyInjection permite centralizar la construcción de objetos de la aplicación.

La inyección de dependencias es un patrón de diseño de software.

Las dependencias no se crean (no new), se inyectan.

Más flexibilidad y reusabilidad.

DependencyInjection

Page 84: Symfony en Drupal 8 - DrupalCamp Spain

DependencyInjectionclass Blog { protected $mailer; protected $posts; ! function __construct() { $this->mailer = new MyMailer(); } ! function addPost(Post $post) { $this->posts[] = $post; $this->sendMail($post); } ! function sendMail(Post $post) { $this->mailer->send(/* ... */); } }

Page 85: Symfony en Drupal 8 - DrupalCamp Spain

DependencyInjectionclass Blog { protected $mailer; protected $posts; ! function __construct() { $this->mailer = new MyMailer(); } ! function addPost(Post $post) { $this->posts[] = $post; $this->sendMail($post); } ! function sendMail(Post $post) { $this->mailer->send(/* ... */); } }

Page 86: Symfony en Drupal 8 - DrupalCamp Spain

DependencyInjectionclass Blog { protected $mailer; protected $posts; ! function __construct() { $this->mailer = new MyMailer(); } ! function addPost(Post $post) { $this->posts[] = $post; $this->sendMail($post); } ! function sendMail(Post $post) { $this->mailer->send(/* ... */); } }

Page 87: Symfony en Drupal 8 - DrupalCamp Spain

DependencyInjectionclass Blog { protected $mailer; protected $posts; ! function __construct(MailerInterface $mailer) { $this->mailer = $mailer; } ! function addPost(Post $post) { $this->posts[] = $post; $this->sendMail($post); } ! function sendMail(Post $post) { $this->mailer->send(/* ... */); } }

Page 88: Symfony en Drupal 8 - DrupalCamp Spain

DependencyInjection

DIC

SMTP …

Mailer

Page 89: Symfony en Drupal 8 - DrupalCamp Spain

DependencyInjection

DIC

SMTP …

$container->get(“Mailer”)

Mailer

Page 90: Symfony en Drupal 8 - DrupalCamp Spain

DependencyInjection

DIC

$container->get(“Mailer”)

Mailer

Page 91: Symfony en Drupal 8 - DrupalCamp Spain

DependencyInjection

DIC

$container->get(“Mailer”)

Mailer

Page 92: Symfony en Drupal 8 - DrupalCamp Spain

DependencyInjection

leanpub.com/a-year-with-symfony

Matthias Noback

Page 93: Symfony en Drupal 8 - DrupalCamp Spain
Page 94: Symfony en Drupal 8 - DrupalCamp Spain

Validator

Page 95: Symfony en Drupal 8 - DrupalCamp Spain

El componente Validator permite validar información de entrada a nuestra aplicación.

Basado en la especificación JSR 303.

Se divide en constraints y validators.

Validator

Page 96: Symfony en Drupal 8 - DrupalCamp Spain

Validator

use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Constraints\Range; $validator = Validation::createValidator(); $constraint = new Range(array( 'min' => 1, 'max' => 10 )); $violations = $validator->validateValue( 15, $constraint );

Page 97: Symfony en Drupal 8 - DrupalCamp Spain

Validator

use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Constraints\Range; $validator = Validation::createValidator(); $constraint = new Range(array( 'min' => 1, 'max' => 10 )); $violations = $validator->validateValue( 15, $constraint );

Page 98: Symfony en Drupal 8 - DrupalCamp Spain

Validator

use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Constraints\Range; $validator = Validation::createValidator(); $constraint = new Range(array( 'min' => 1, 'max' => 10 )); $violations = $validator->validateValue( 15, $constraint );

Page 99: Symfony en Drupal 8 - DrupalCamp Spain

Validator

use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Constraints\Range; $validator = Validation::createValidator(); $constraint = new Range(array( 'min' => 1, 'max' => 10 )); $violations = $validator->validateValue( 15, $constraint );

Page 100: Symfony en Drupal 8 - DrupalCamp Spain

Validator

use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Constraints\Range; $validator = Validation::createValidator(); $constraint = new Range(array( 'min' => 1, 'max' => 10 )); $violations = $validator->validateValue( 15, $constraint );

Page 101: Symfony en Drupal 8 - DrupalCamp Spain

Validator

NotBlank Ip DateTime CardScheme

Blank Range Time Currency

NotNull EqualTo Choice Luhn

Null NotEqualTo Collection Iban

True IdenticalTo Count Isbn

False NotIdenticalTo UniqueEntity Issn

Type LessThan Language Callback

Email LessThanOrEqual Locale Expression

Length GreaterThan Country All

Url GreaterThanOrEqual File UserPassword

Regex Date Image Valid

Page 102: Symfony en Drupal 8 - DrupalCamp Spain
Page 103: Symfony en Drupal 8 - DrupalCamp Spain

Serializer

Page 104: Symfony en Drupal 8 - DrupalCamp Spain

El componente Serializer convierte objetos PHP en otros formatos (p.ej. JSON) y viceversa.

Está diseñado para que pueda ser extensible.

Serializer

Page 105: Symfony en Drupal 8 - DrupalCamp Spain

Serializer

Objeto FormatoArray

Serialización

Deserialización

Page 106: Symfony en Drupal 8 - DrupalCamp Spain
Page 107: Symfony en Drupal 8 - DrupalCamp Spain

Yaml

Page 108: Symfony en Drupal 8 - DrupalCamp Spain

El componente Yaml parsea y serializa archivos en formato YAML (YAML Ain't Markup Language).

YAML es un formato de serialización de datos amigable para humanos.

Soporte para múltiples lenguajes de programación.

Yaml

Page 109: Symfony en Drupal 8 - DrupalCamp Spain

Yaml

talks: 0: title: "Symfony en Drupal 8" speaker: "Raúl Fraile" description: "La versión 8 de Drupal…” datetime: 2014-05-18T10:00:00+02:00

Page 110: Symfony en Drupal 8 - DrupalCamp Spain

Yaml

talks: 0: title: "Symfony en Drupal 8" speaker: "Raúl Fraile" description: "La versión 8 de Drupal…” datetime: 2014-05-18T10:00:00+02:00

array (1) · [talks]: array (1) · · [0]: array (4) · · · [title]: string (19) "Symfony en Drupal 8" · · · [speaker]: string (11) "Raúl Fraile" · · · [description]: string (464) "La versión 8 de Drupal…” · · · [datetime]: int 1400400000

Page 111: Symfony en Drupal 8 - DrupalCamp Spain

Yaml

use Symfony\Component\Yaml\Parser; use Symfony\Component\Yaml\Dumper; !$parser = new Parser(); $data = $parser->parse( file_get_contents(‘data.yml') ); !$dumper = new Dumper(); $yaml = $dumper->dump($data); file_put_contents('data2.yml', $yaml);

Page 112: Symfony en Drupal 8 - DrupalCamp Spain

Recursos

Page 113: Symfony en Drupal 8 - DrupalCamp Spain

Recursos

symfony.es/libro

Javier Eguiluz

Page 114: Symfony en Drupal 8 - DrupalCamp Spain

Recursos

blog.servergrove.com/tag/symfony2-components

Page 115: Symfony en Drupal 8 - DrupalCamp Spain

Gràcies!

@raulfraile

VPS100-700: 15% mes: 'DPSpain15' VPS100-700: 20% mes: 'DPSpain20'