The format – Talks then Hackingassets.en.oreilly.com/1/event/61/Top Shelf PHP Presentation...

Post on 01-Feb-2018

219 views 0 download

Transcript of The format – Talks then Hackingassets.en.oreilly.com/1/event/61/Top Shelf PHP Presentation...

The format – Talks then Hacking

Follow along with the sandbox

What is Symfony2?

A web framework

A web framework is a software that providesgeneric components that ease application development

and team work.

A framework helps developers create flexible and extensible applications, write quality and maintainable code.

Symfony2 is built on top of PHP 5.3

It especially uses the new namespace system

git clone https://github.com/jmikola/top-shelf-php.git

IDEs Integration

Eclipse: https://github.com/pulse00/Twig-Eclipse-Plugin

Netbeans: https://github.com/blogsh/Twig-netbeans

PHPStorm: As of 2.1

VIM: http://jinja.pocoo.org/2/documentation/integration

TextMate: https://github.com/Anomareh/PHP-Twig.tmbundle

Install the standard distribution

php app/check.php

Requirements checks

# web/app_dev.phprequire_once __DIR__.'/../app/AppKernel.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('dev', true);$kernel->handle(Request::createFromGlobals())->send();

The front controller is the single entry point of the application

The front controller

http://www.domain.tld/web/app_dev.php/

http://www.domain.tld/web/app_dev.php/hello/Fabien

http://www.domain.tld/web/app_dev.php/hello/Toto

The front controller

Every resource the user wants to access is available from the front controller

http://www.domain.tld/

http://www.domain.tld/hello/Fabien

http://www.domain.tld/hello/Toto

If the virtual host is correctly configured to make the web/ folder as the web root directory, URI becomes as follow.

The front controller

<VirtualHost *:80> ServerName www.domain.tld DocumentRoot "/path/to/sandbox/web" DirectoryIndex app.php <Directory "/path/to/sandbox/web"> AllowOverride All Allow from All </Directory></VirtualHost>

Production configuration sample for Apache

In production environment, make sure your Apache is configured to make the web/ folder as the web root directory of your application.

# web/.htaccess<IfModule mod_rewrite.c>

RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ app.php [QSA,L]

</IfModule>

Production configuration sample for Apache

The web/.htaccess file redirects every request on the front controller.

๏ app/ is the application folder

๏ src/ is the libraries folder

๏ web/ is the general public folder

The app/ folder contains the configuration and generated files

The src/ folder contains the PHP code

The web/ folder contains front controllers and web assets (images, Javascripts, stylesheets, ...)

.|-- LICENSE|-- README|-- app/| |-- AppCache.php| |-- AppKernel.php| |-- cache/| |-- config/| |-- console| |-- logs/| |-- autoload.php| |-- check.php| `-- phpunit.xml.dist|-- bin/|-- src/| `-- Acme/|-- vendor/`-- web/ |-- config.php |-- app.php `-- app_dev.php

Architecture

web/should be the web root directory

app/cache/ and app/logs/

must be writable by the web server

Architecture

An Application is a directorycontaining the configurationfor a given set of Bundles

Architecture

A Bundle is a structured set of files(PHP files, stylesheets, JavaScripts, images, ...)

that implements a single feature (a blog, a forum, ...)and which can be easily shared with other developers.

Architecture

Accessing a Resource

Simply browse the following URIhttp://…/…/app_dev.php/hello/Fabien

Virtual path to the resource the user wants to access

Symfony2 provides a routing mechanism that converts a typical URI into a web response

๏To link URIs to the application internal actions

๏To improve SEO optimizations and bookmarks

๏To be able to create or change URIs with ease

๏To avoid to show sensible information

๏To give meaningful URIs to the end user

Architecture

๏They show sensible information

๏They mention variable names and values

๏They are not meaningful and optimized for SEO engines

http://www.domain.com/blog.php?action=show&article_id=123

Default URLs have some problems…

๏Clean and smart URIs to give meaningful information

๏No sensible information or variables

๏Easy URIs configuration in YAML, PHP or XML

http://www.domain.com/blog/2010/09/15/symfony2-rocks

URL rewriting is better !

The Router is responsible to match

a Pattern with a corresponding Controller.

The Router is responsible to match

a Pattern with a corresponding Controller.

A Controller is responsible to process a

Request and to return a Response

How does the routing work?

How to process a request?

The controller is responsible to convert a request into a response

Generating a response

namespace Sensio\HelloBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;use Symfony\Component\HttpFoundation\Response;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class HelloController extends Controller{ /** @Route("/hello/{name}", name="greet") */ public function indexAction($name) { return new Response(sprintf('Hello %s!', $name)); }}

namespace Sensio\HelloBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class HelloController extends Controller{ /** @Route("/hello/{name}", name="greet") */ public function indexAction($name) { return $this->render('SensioHelloBundle:Hello:index.html.twig', array( 'name' => $name )); }}

Generating a response

Unlike previous examples, the render() method decouples the application logic of the controller from the rendering of the view layer.

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class HelloController extends Controller{ /** * @Route("/hello/{name}", name="greet") * @Template("SensioHelloBundle:Hello:index.html.twig") */ public function indexAction($name) { return array('name' => $name); }}

Variables will be sent to the template thanks to the array

Generating a response

The rendered template can also be mapped with a special annotation coming from the SensioFrameworkExtraBundle bundle.

Variables will be sent to the template thanks to the array

Generating a response

If the logical path to the template follows the following naming convention (BundleName:ControllerName:actionName.html.twig), the logical path can be omitted from the @Template annotation.

class HelloController extends Controller{ /** * @Route("/hello/{name}", name="greet") * @Template() */ public function indexAction($name) { return array('name' => $name); }}

The route name to generate links or uris.

Diving into the view layer

Twig fundamentals

What is Twig?

Twig is a modern template engine for PHP

Fast: Twig compiles templates down to plain optimized PHP code. The overhead compared to regular PHP code was reduced to the very minimum.

Secure: Twig has a sandbox mode to evaluate untrusted template code. This allows Twig to be used as a template language for applications where users may modify the template design.

Flexible: Twig is powered by a fexible lexer and parser. This allows the developer to define its own custom tags and filters, and create its own DSL.

Twig has several tags (markers) to indicate the parser how to process the code. Every marker starts by an opening brace character – {– and ends with a closing brace – } .

Tag Meaning

{{ a_variable }} Outputs an escaped string

{# Some commented code #} Comments a piece of code

{% if glass is empty %} Evaluates an expression (block, loop, condition…)

Twig syntax

Simply use the double curly braces statement, {{ }}, which is equivalent to the echo / print statement in PHP.

{{ }} always outputs something!

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"><html lang="en"> <head> <title>My Webpage</title> </head> <body> <p> Hello, my name is {{ name }}! </p> </body></html>

Printing variables

Twig accepts the following syntax to deal with both objects and arrays

# foo is an array|object and bar a valid index|property|method{{ foo.bar }}

# foo is an array and 0 a valid index# foo is an object that implements ArrayAccess interface{{ foo.0 }}

# foo is an array and bar a valid index.# foo is an object that implements ArrayAccess interface{{ foo['bar'] }}

# bar is a variable containing the index in the foo array{{ foo[bar] }}

Dealing with objects and arrays

When using foo.bar

Check if foo is an array and bar a valid element;

If not, and if foo is an object, check that bar is a valid property;

If not, and if foo is an object, check that bar is a valid method;

if bar is the constructor - use __construct() instead;● If not, and if foo is an object, check that getBar is a valid method;● If not, and if foo is an object, check that isBar is a valid method;● If not, return a null value.

How Twig deals with variables?

When using foo[‘bar’]

Check if foo is an array and bar a valid element;

If not, return a null value.

# foo is an array and 0 a valid index{{ foo.0 }}

# foo is an array and bar a valid index.{{ foo['bar'] }}

# bar is a variable containing the index in the foo array{{ foo[bar] }}

How Twig deals with variables?

The « for » statement loops over each item in a sequence. For example, to display a list of users provided in a variable called users:

The for() loop accepts any array or objects that implements the Traversable interface.

<h1>Members</h1>

<ul> {% for user in users %} <li>{{ user.username }}</li> {% endfor %}</ul>

For loop

Iterating over a sequence of numbers

Iterating over a sequence of letters

The .. operator can take any expression at both sides.

If you need a step different from 1, you can use the range function instead.

{% for i in 0..10 %} * {{ i }}{% endfor %}

{% for letter in 'a'..'z' %} * {{ letter }}{% endfor %}

{% for letter in 'a'|upper..'z'|upper %} * {{ letter }}{% endfor %}

{% for i in range(0, 10, 2) %} * {{ i }}{% endfor %}

For loop

Inside of a for loop block you can access some special variables. Instead, note that you can’t break or continue in a loop.

loop.index The current iteration of the loop. (1 indexed)

loop.index0 The current iteration of the loop. (0 indexed)

loop.revindex The number of iterations from the end of the loop (1 indexed)

loop.revindex0 The number of iterations from the end of the loop (0 indexed)

loop.first True if first iteration

loop.last True if last iteration

loop.length The number of items in the sequence

loop.context The parent context

For loop

If the array is empty or null, you can output something else instead.

If you want to iterate over the array keys, simple use the keys filter.

<ul>

{% for user in users %} <li>{{ user.username }}</li> {% else %} <li><em>no user found</em></li> {% endfor %}

</ul>

<h1>Members</h1>

<ul>

{% for key in users|keys %} <li>{{ key }}</li> {% endfor %}

</ul>

For loop

Of course, you can grab both key and value in a row.

<h1>Members</h1>

<ul>

{% for key, user in users %} <li>{{ key }}: {{ user.username }}</li> {% endfor %}

</ul>

For loop

Twig also brings an « if » conditional statement to perform decisions.

The « is defined » statement is called a « test » in Twig vocabulary. It checks that the variable is defined or not.

{% if users is defined %} <ul> {% for user in users %} <li>{{ user.username }}</li> {% endfor %} </ul>{% endif %}

Conditional statements

Of course, you can expand the « if » conditional statement to « elseif » and « else » clauses as shown in the following snippet.

{% if kenny.sick %}

Kenny is sick.

{% elseif kenny.dead %}

You killed Kenny! You bastard!!!

{% else %}

Kenny looks okay --- so far

{% endif %}

Conditional statements

Experimenting Twig

The controller

Implement your first controller to render « Hello World ».

# src/Acme/TodoBundle/Controller/DefaultController.phpnamespace Sensio\Bundle\TrainingBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;use Symfony\Component\HttpFoundation\Response;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class DefaultController extends Controller{ /** @Route("/hello/{name}", name="greet") */ public function indexAction($name) { return new Response(sprintf('Hello %s!', $name)); }}

Simplify the action code by adding a new extra annotation to the method php documentation to specify which template to render.

# src/Acme/TodoBundle/Controller/DefaultController.phpuse Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class DefaultController extends Controller{ /** * @Route("/hello/{name}", name="greet") * @Template("AcmeTodoBundle:Default:index.html.twig") */ public function indexAction($name) { return array('name' => $name); }}

Simplify the @Template annotation to let Symfony guess the template to render based on coding convention.

# src/Acme/TodoBundle/Controller/DefaultController.phpuse Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class DefaultController extends Controller{ /** * @Route("/hello/{name}", name="greet") * @Template() */ public function indexAction($name) { return array('name' => $name); }}

Assetic presented by Kris WallsmithAsset Management for PHP 5.3

Lots of awesome tools:CoffeeScript

Compass Framework

CSSEmbed

Google Closure Compiler

JSMin

LESS

Packer

SASS

Sprockets

Stylus

YUI Compressor

Assetic makes using these easy

# /path/to/web/js/core.php$core = new FileAsset('/path/to/jquery.js');

$core->load();

header('Content-Type: text/javascript');

echo $core->dump();

# /path/to/web/js/core.php

$core = new AssetCollection(array(

new FileAsset('/path/to/jquery.js'),

new GlobAsset('/path/to/js/core/*.js'),

));

$core->load();

header('Content-Type: text/javascript');

echo $core->dump();

# /path/to/web/js/core.php

$core = new AssetCollection(array(

new FileAsset('/path/to/jquery.js'),

new GlobAsset('/path/to/js/core/*.js'),

), array(

new YuiCompressorJsFilter('/path/to/yui.jar'),

));

$core->load();

header('Content-Type: text/javascript');

echo $core->dump();

<script src="js/core.php"></script>

Assetic isAssets & Filters

Filters

Inspired by Python’s webassets

https://github.com/miracle2k/webassets

Assets have lazy, mutable content

A filter acts on an asset’s contents during “load”

and “dump”

Assets can be gathered in collections

A collection is an asset

Basic Asset Classes•AssetCollection

•AssetReference

•FileAsset

•GlobAsset

•HttpAsset

•StringAsset

Core Filter ClassesCoffeeScriptFilter

CompassFilter

CssEmbedFilter

CssImportFilter

CssMinFilter

CssRewriteFilter

GoogleClosure\CompilerApiFilter

GoogleClosure\CompilerJarFilter

JpegoptimFilter

JpegtranFilter

LessFilter

LessphpFilter

OptiPngFilter

PackagerFilter

Core Filter ClassesPngoutFilter

Sass\SassFilter

Sass\ScssFilter

SprocketsFilter

StylusFilter

Yui\CssCompressorFilter

Yui\JsCompressorFilter

More to come…

Twig Integration

{% javascripts 'js/*.coffee' filter='coffee,?yui_js' %}

<script src="{{ asset_url }}"></script>

{% endjavascripts %}

<script src="js/92429d8.js"></script>

{% javascripts 'js/*.coffee' filter='coffee,?closure'

debug=true %}

<script src="{{ asset_url }}"></script>

{% endjavascripts %}

<script src="js/92429d8_1.js"></script>

<script src="js/92429d8_2.js"></script>

<script src="js/92429d8_3.js"></script>

AsseticBundleSymfony2 integration

assetic:

debug: %kernel.debug%

use_controller: %kernel.debug%

filters:

coffee: ~

yui_js:

jar: /path/to/yui.jar

{# when use_controller=true #}

<script src="{{ path('assetic_foo') }}"...

# routing_dev.yml_assetic: resource: .

type: assetic

{# when use_controller=false #}<script src="{{ asset('js/core.js') }}"></script>

The Symfony2 Assets Helper

•Multiple asset domains

•Cache buster

framework:

templating:

assets_version: 1.2.3

assets_base_urls:

- http://assets1.domain.com

- http://assets2.domain.com

- http://assets3.domain.com

- http://assets4.domain.com

<link href="http://assets3.domain.com/css/all.css?1.2.3" ...

assetic:dump

$ php app/console assetic:dump web/

assetic:dump --watchDump static assets in the background as you develop

Error Management

Error Management

Symfony2 is HTTP compliant as it’s able to send the corresponding status code depending on the leveraged error or exception when the request is handled.

Persisting data between HTTP requests

Persisting data between HTTP requests

Cookies and sessions were invented to offer a mean to simulate persistence between two different HTTP requests as the HTTP is a stateless protocol.

The Request object API

# Getting the request service$request = $this->getRequest();

# Wether the request comes from an Ajax call$request->isXmlHttpRequest();

# Find the preferred language based on Accept-Language header$request->getPreferredLanguage(array('en', 'fr'));

# Get a $_GET parameter$request->query->get('page');

# Get a $_POST parameter$request->request->get('content');

# Get a $_COOKIE parameter$request->cookies->get('remember-me');

# Get a $_SERVER parameter$request->server->get('SCRIPT_NAME');

{# Wether the request comes from an Ajax call #}{% if app.request.isXmlHttpRequest %}

{# Find the preferred language based on Accept-Language header #}{{ app.request.getPreferredLanguage(['en', 'fr']) }}

{# Get a $_GET parameter #}{{ app.request.query.get('page') }}

{# Get a parameter #}{{ app.request.parameter.get('content') }}

{# Get a $_COOKIE parameter #}{{ app.request.cookies.get('remember-me') }}

{# Get a $_SERVER parameter #}{{ app.request.server.get('SCRIPT_NAME') }}

The Request object from Twig

# Getting a response$response = new Response('<html>...</html>');$response = $this->render('FooBundle:Bar:template.html.twig');

# Triggering a redirect response$response = new RedirectResponse('http://www.domain.tld/job/8');$response = $this->redirect($this->generateUrl('home'));

# Fixing HTTP Expiration headers$response->setExpires(new \DateTime('2011-10-01 11:10:00'));$response->setMaxAge(120);$response->setSharedMaxAge(120);

# Fixing HTTP Validation headers$response->setLastModified(new \DateTime('now'));$response->setETag('a1b2c3');

The Response object API

More with Templates

Create a new « menu.html.twig » template in the « AcmeTodoBundle » bundle that contains the following code.

Include the menu in the header and footer sections of the layout.

{# src/Acme/TodoBundle/Resources/views/menu.html.twig #}<div class="navigation"> <a href="#">Home</a> | <a href="#">About</a> | <a href="#">Contact</a></div>

{# src/Acme/TodoBundle/Resources/views/layout.html.twig #}{% block body %}

<div id="header">{% include 'AcmeTodoBundle::menu.html.twig' %}</div>

<h1>Training Application</h1> {% block content %}{% endblock %} <div id="footer">{% include 'AcmeTodoBundle::menu.html.twig' %}</div>

{% endblock %}

Diving into the Model layer

Symfony2 is a Model View Controller framework!

The Client makes a Request on the Server…

The Request hits the Front Controller.

The Front Controller dispatches to the corresponding Controller…

The Controller handles the Request, calls the Model, passes data to the View,

and finally returns a Response…

The Response is sent to the Client.

In the MVC pattern, the Model is the most important layer as it stores data and business logic to access and modify them.

Doctrine MongoDB ODMKris Wallsmith

This is MongoDB…

$mongo = new Mongo();

$db = $mongo->pdxphp;

$db->people->save(array(

'name' => 'Kris Wallsmith',

));

$cursor = $db->people->find();

print_r(iterator_to_array($cursor));

Array( [4cbdffdae84ded424f000000] => Array (

[_id] => MongoId Object

[name] => Kris Wallsmith ))

MongoDB is where youput your arrays for later.

$db->people->save(array(

'name' => 'Sam Keen',

'roles' => array(

'organizer',

'presenter',

),

));

$query = array('roles' => 'presenter');

$cursor = $db->people->find($query);

print_r(iterator_to_array($cursor));

Array( [4cbe03cfe84dedb850010000] => Array ( [_id] => MongoId Object [name] => Sam Keen [roles] => Array ( [0] => organizer [1] => presenter ) ))

Me too!

$query = array(

'name' => 'Kris Wallsmith',

);

$kris = $db->people->findOne($query);

$kris['roles'] = array('presenter');

$db->people->save($kris);

$query = array('roles' => 'presenter');

$fields = array('name');

$cursor = $db->people->find($query, $fields);

print_r(iterator_to_array($cursor));

Array( [4cbe0a9de84ded7952010000] => Array ( [_id] => MongoId Object [name] => Sam Keen ) [4cbe0a9de84ded7952000000] => Array ( [_id] => MongoId Object [name] => Kris Wallsmith ))

Be surgical.

$query = array('roles' => 'presenter');

$update = array(

'$push' => array(

'roles' => 'cool guy',

),

);

$db->people->update($query, $update);

Atomic Operators

$inc

$set

$unset

$push

$pushAll

$addToSet

$pop

$pull

$pullAll

$rename

Advanced Queries

$roles = array('organizer', 'presenter');

$db->people->find(array(

'roles' => array('$all' => $roles),

));

Conditional Operators

$ne

$in

$nin

$mod

$all

$size

$exists

$type

$or

$elemMatch

Cursor Methods

$cursor = $db->people->find();$cursor->sort(array('name' => 1));foreach ($cursor as $person) { // ...

}

I like you, Sam.

$samRef = MongoDBRef::create('people', $samId);$db->people->update( array('_id' => $kris['_id']), array( '$addToSet' => array( 'likes' => $samRef, ), )

);

$sam = $db->getDBRef($samRef);

$db->people->find(array(

'likes.$id' => $kris['_id'],

));

TerminologyRDBMSRDBMS MongoDBMongoDB

Database Database

Table Collection

Row Document

Foreign Key Database Reference

A document is an array.

Arrays are nice.

Objects are better.*

* Whenever objects are better.

The Doctrine MongoDBObject Document Mapper

maps documentsto and from objects.

We just need to tell it how.

/** @Document(collection="people") */

class Person {

/** @Id */

public $id;

/** @String */

public $name;

/** @Collection */

public $roles = array();

/** @ReferenceMany */

public $likes = array();

/** @EmbedMany(targetDocument="Address") */

public $addresses = array();

}

POPO FTW!

$kris = new Person();

$kris->name = 'Kris Wallsmith';

$kris->roles[] = 'presenter';

$kris->likes[] = $sam;

$kris->addresses[] = $homeAddy;

$documentManager->persist($kris);

$documentManager->flush();

Wherefore art thou->save()

???

ActiveRecord is more abstract.

Doctrine calculates theoptimal query for you.optimal query for you.

$kris = $dm->findOne('Person', array(

'name' => 'Kris Wallsmith',

));

$kris->roles[] = 'cool guy';

$dm->flush();

$db->people->update(array(

'_id' => $kris->id,

), array(

'$push' => array(

'roles' => 'cool guy',

),

));

Query API

$qb = $dm->createQueryBuilder('Person')

->field('name')->notEqual('Kris Wallsmith')

->field('roles')->equals('presenter')

->sort('name', 'asc');

$cursor = $qb->getQuery()->execute();

Lifecycle Callbacks

/** @Document */

class Foo {

/** @Date */

public $createdAt;

/** @PrePersist */

public function ensureCreatedAt() {

$this->createdAt = new DateTime();

}

}

That’s MVC!

Configuration principles

Symfony2 supports several configuration file formats out of the box :

YAML, XML, INI or PHP code.

Choosing the best file format

Pros Cons

XML ValidationIDE completion & help Verbose (not that much)

INI ConciseEasy to read and to change Limited syntax

YAML ConciseEasy to read and to change Hard to validate / YAML component

PHP Flexible, more expressive No validation

Uncomment code to activate XML or PHP configuration

Default configuration format

# app/AppKernel.phpclass AppKernel extends Kernel{ # ... public function registerContainerConfiguration(LoaderInterface $loader) { // use YAML for configuration // comment to use another configuration format $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');

// uncomment to use XML for configuration //$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.xml');

// uncomment to use PHP for configuration //$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.php'); }}

YAML configuration sample

# config/config_dev.ymlimports: - { resource: config.yml }

app.config: router: resource: "%kernel.root_dir%/config/routing_dev.yml"

profiler: { only_exceptions: false }

webprofiler.config: toolbar: true intercept_redirects: true

XML configuration sample

<?xml version="1.0" ?><container xmlns="http://www.symfony-project.org/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:zend="http://www.symfony-project.org/schema/dic/zend" xmlns:app="http://www.symfony-project.org/schema/dic/symfony" xmlns:webprofiler="http://www.symfony-project.org/schema/dic/webprofiler" xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd http://www.symfony-project.org/schema/dic/webprofiler http://www.symfony-project.org/schema/dic/webprofiler/webprofiler-1.0.xsd http://www.symfony-project.org/schema/dic/zend http://www.symfony-project.org/schema/dic/zend/zend-1.0.xsd »> <imports> <import resource="config.xml" /> </imports> <app:config> <app:router resource="%kernel.root_dir%/config/routing_dev.xml" /> <app:profiler only-exceptions="false" /> </app:config> <webprofiler:config toolbar="true" intercept-redirects="true" /> <zend:config> <zend:logger priority="info" path="%kernel.logs_dir%/%kernel.environment%.log" /> </zend:config></container>

PHP configuration sample

$loader->import('config.php');

$container->loadFromExtension('app', 'config', array( 'router' => array('resource' => '%kernel.root_dir%/config/routing_dev.php'), 'profiler' => array('only-exceptions' => false),));

$container->loadFromExtension('webprofiler', 'config', array( 'toolbar' => true, 'intercept-redirects' => true,));

$container->loadFromExtension('zend', 'config', array( 'logger' => array( 'priority' => 'info', 'path' => '%kernel.logs_dir%/%kernel.environment%.log', ),));

Global configuration can be overloadedaccording to the environment you are on…

Overriding and overloading principles

# config/config_dev.ymlimports: - { resource: config.yml } - { resource: "@FooBarBundle/Resources/config/foo.xml" }

app.config: router: { resource: "%kernel.root_dir%/config/routing_dev.yml" } profiler: { only_exceptions: false }

webprofiler.config: toolbar: true intercept_redirects: true

Importing INI files

# app/config/config.ymlimports: - { resource: dice.config.ini }

parameters: available_colors: [red, green, blue, purple, yellow, black]

# app/config/dice.config.ini[parameters]dice.min = 1dice.max = 6

Forms

How does it work?

1) The form reads properties of an object,

2) Based on these values, form fields are prepopulated,

3) When the form is submitted, data are bound to the form,

4) Then, the validation business logic is applied on each field ,

5) If validation fails, form is displayed again with submitted values,

6) Otherwise, form is processed.

Creating a new formnamespace Acme\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;use Acme\DemoBundle\Entity\Product;

class ShopController extends Controller{ /** * @Route("/product", name="_new_product") * @Template */ public function indexAction() { $product = new Product(); $product->name = 'Test product'; $product->setPrice('50.00');

$form = $this->createFormBuilder($product) ->add('name', 'text') ->add('price', 'money', array('currency' => 'USD')) ->getForm();

return array('form' => $form->createView()); }}

Prototyping the form rendering

Symfony provides a dedicated Twig function to automate the rendering of a form in a template.

<form action="#" method="post">

{{ form_widget(form) }}

<button type="submit">Create the product</button></form>

Prototyping the form rendering

Symfony provides a dedicated Twig function to automate the rendering of a form in a template.

<form action="#" method="post">

{{ form_widget(form) }}

<button type="submit">Create the product</button></form>

Creating a dedicated form class

To make the controller thinner and the form reusable anywhere, the best practice is

move the form creation code to a dedicated form class.

// src/Acme/DemoBundle/Form/ProductType.phpnamespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;use Symfony\Component\Form\FormBuilder;

class ProductType extends AbstractType{ public function buildForm(FormBuilder $builder, array $options) { $builder->add('name'); $builder->add('price', 'money', array( 'currency' => 'USD' )); }}

Creating a custom form

Using a form type from the controller

use Acme\DemoBundle\Entity\Product;use Acme\DemoBundle\Form\ProductType;

// ...

public function indexAction(){ $product = new Product(); $product->name = 'A name'; $product->setPrice(50.00);

$form = $this->createForm(new ProductType(), $product);

// ...}

Experimenting Forms

The contact request form

oBasic form composed of 3 fields (sender, subject, message)

oForm will validate a ContactRequest object

oThe ContactRequest properties are constrainted with validators

First, create the contact type class in the AcmeTodoBundle bundle.

// src/Acme/TodoBundle/Form/ContactRequestType.phpnamespace Sensio\Bundle\TrainingBundle\Form;

use Symfony\Component\Form\AbstractType;use Symfony\Component\Form\FormBuilder;

class ContactRequestType extends AbstractType{ public function buildForm(FormBuilder $builder, array $options) { $builder->add('sender', 'text'); $builder->add('subject', 'text'); $builder->add('message', 'textarea'); }}

The contact request domain object

The sender value is mandatory and must be a valid email address

namespace Sensio\TrainingBundle\Form;

use Symfony\Component\Validator\Constraints as Assert;

class ContactRequest{ /** * @Assert\Email() * @Assert\NotBlank() */ public $sender;

/** * @Assert\MaxLength(50) * @Assert\NotBlank() */ public $subject;

/** @Assert\NotBlank() */ public $message;}

The subject value is mandatory and can’t exceed 50 characters

namespace Sensio\TrainingBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;use Symfony\Component\HttpFoundation\RedirectResponse;use Symfony\Component\HttpFoundation\Request;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;use Sensio\TrainingBundle\Form\ContactRequest;use Sensio\TrainingBundle\Form\ContactRequestType;

class ContactController extends Controller{ /** * @Route("/contact", name="contact") * @Template */ public function indexAction(Request $request) { $contact = new ContactRequest(); $form = $this->createForm(new ContactRequestType(), $contact);

if ($request->getMethod() == 'POST') { $form->bindRequest($request);

if ($form->isValid()) { // ... send an email return $this->redirect($this->generateUrl('contact_success')); } }

return array('form' => $form->createView()); }}

Prototyping the form rendering

Symfony provides a dedicated Twig function to automate the rendering of a form in a template.

<form action="#" method="post">

{{ form_widget(form) }}

<button type="submit">Create the product</button></form>

Implementing the success page

{# @AcmeTodoBundle/Resources/views/Contact/thankYou.html.twig #}

{% extends 'AcmeTodoBundle::layout.html.twig' %}

{% block content %}

<h2>Contact Us</h2> <p>Thanks for your message</p>

{% endblock %}

/** * @Route("/contact/thank-you", name="contact_success") * @Template */public function thankYouAction(){ return array();}

In order to verify application behaviorAs a software developer

I need tests

In order to verify application behaviorAs a software developer

I need tests

Preferably automated tests

Test-Driven Development...is an iterative design process

● Write a test

Test-Driven Development...is an iterative design process

● Write a test● Ensure the new test fails

Test-Driven Development...is an iterative design process

● Write a test● Ensure the new test fails● Write code to satisfy the test

Test-Driven Development...is an iterative design process

● Write a test● Ensure the new test fails● Write code to satisfy the test● Ensure all tests pass

Test-Driven Development...is an iterative design process

● Write a test● Ensure the new test fails● Write code to satisfy the test● Ensure all tests pass● Refactor

Test-Driven Development...is an iterative design process

● Write a test● Ensure the new test fails● Write code to satisfy the test● Ensure all tests pass● Refactor● Repeat

Dan North Introduces BDD

I had a problem. While using and teaching agile practices like test-driven development (TDD) on projects in different environments, I kept coming across the same confusion and misunderstandings. Programmers wanted to know:● Where to start● What to test and what not to test● How much to test in one go● What to call their tests● How to understand why a test fails

http://dannorth.net/introducing-bdd/

I started using the word “behavior” in place of “test” in my dealings with TDD and… I now had answers to some of those TDD questions:● What to call your test is easy – it’s a sentence describing the next behavior in which you are interested.● How much to test becomes moot – you can only describe so much behavior in a single sentence.● When a test fails, simply work through the process described above – either you introduced a bug, the

behavior moved, or the test is no longer relevant.

http://dannorth.net/introducing-bdd/

Dan North Introduces BDD

Behavior-Driven Development...builds upon TDD

● Write test cases in a natural language

Behavior-Driven Development...builds upon TDD

● Write test cases in a natural language

•Understood by developers and business folks alike

Behavior-Driven Development...builds upon TDD

● Write test cases in a natural language

•Understood by developers and business folks alike

•Helps relate domain language of requirements to the code

Behavior-Driven Development...builds upon TDD

● Write test cases in a natural language

•Understood by developers and business folks alike

•Helps relate domain language of requirements to the code● Do this with user stories and scenarios

Behavior-Driven Development...builds upon TDD

● Write test cases in a natural language

•Understood by developers and business folks alike

•Helps relate domain language of requirements to the code● Do this with user stories and scenarios

•User stories describe a feature's benefit in context

Behavior-Driven Development...builds upon TDD

● Write test cases in a natural language

•Understood by developers and business folks alike

•Helps relate domain language of requirements to the code● Do this with user stories and scenarios

•User stories describe a feature's benefit in context

•Scenarios are executable acceptance criteria

Behavior-Driven Development...builds upon TDD

● Write test cases in a natural language

• Understood by developers and business folks alike

• Helps relate domain language of requirements to the code● Do this with user stories and scenarios

• User stories describe a feature's benefit in context

• Scenarios are executable acceptance criteria

A story’s behavior is simply its acceptance criteria – if the system fulfills all the acceptance criteria, it’s behaving correctly; if it doesn’t, it isn’t.

http://dannorth.net/introducing-bdd/

So what does this look like?

This is where Behat and Mink come in.

This is where Behat and Mink come in.

Acceptance testing (any tests)

Tests a feature by executing its scenarios' steps in a context.

Web acceptance testing (functional tests)

Drivers for Goutte, Sahi and Symfony2's test client.

Initialize Our Bundle With Behat

$ app/console behat --init @AcmeDemoBundle

+d src/Acme/DemoBundle/Features

- place your *.feature files here

+f src/Acme/DemoBundle/Features/Context/FeatureContext.php

- place your feature related code here

● We now have a directory to hold AcmeDemoBundle's features● Behat also creates an empty FeatureContext class, which extends BehatBundle's

BehatContext

• Features describe our behavior, but the context tells Behat how to evaluate our feature as an executable test

Let's Have Behat Analyze Our Feature

$ app/console behat src/Acme/DemoBundle/Features/contact.feature

Feature: Contact form

In order to contact an email address

As a visitor

I need to be able to submit a contact form

Scenario: Successfully submit the contact form # contact.feature:6

Given I am on "/demo/contact"

When I fill in "Email" with "user@example.com"

And I fill in "Message" with "Hello there!"

And I press "Send"

Then I should see "Message sent!"

1 scenario (1 undefined)

5 steps (5 undefined)

Behat Creates the Glue...but the rest is up to you

/**

* @Given /^I am on "([^"]*)"$/

*/

public function iAmOn($argument1)

{

throw new PendingException();

}

/**

* @When /^I fill in "([^"]*)" with "([^"]*)"$/

*/

public function iFillInWith($argument1, $argument2)

{

throw new PendingException();

}

/**

* @Given /^I press "([^"]*)"$/

*/

public function iPress($argument1)

{

throw new PendingException();

}

/**

* @Then /^I should see "([^"]*)"$/

*/

public function iShouldSee($argument1)

{

throw new PendingException();

}

You can implement step definitions for undefined steps with these snippets:

Not so fast. What about Mink?

MinkContext Defines Steps...for making requests

Pattern DescriptionGiven /^I am on "(?P<page>[^"]+)"$/ Opens specified pageWhen /^I go to "(?P<page>[^"]+)"$/ Opens specified pageWhen /^I reload the page$/ Reloads current pageWhen /^I move backward one page$/ Moves backward one page in historyWhen /^I move forward one page$/ Moves forward one page in historyWhen /^I press "(?P<button>(?:[^"]|\\")*)"$/

Presses button with specified id|name|title|alt|value

When /^I follow "(?P<link>(?:[^"]|\\")*)"$/ Clicks link with specified id|title|alt|text

MinkContext Defines Steps...for interacting with forms

Pattern DescriptionWhen /^I fill in "(?P<field>(?:[^"]|\\")*)" with "(?P<value>(?:[^"]|\\")*)"$/ Fills in form field with specified id|name|label|value

When /^I fill in "(?P<value>(?:[^"]|\\")*)" for "(?P<field>(?:[^"]|\\")*)"$/ Fills in form field with specified id|name|label|value

When /^I fill in the following:$/ Fills in form fields with provided table

When /^I select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/ Selects option in select field with specified id|name|label|value

When /^I check "(?P<option>(?:[^"]|\\")*)"$/ Checks checkbox with specified id|name|label|value

When /^I uncheck "(?P<option>(?:[^"]|\\")*)"$/ Unchecks checkbox with specified id|name|label|value

When /^I attach the file "(?P<path>[^"]*)" to "(?P<field>(?:[^"]|\\")*)"$/ Attaches file to field with specified id|name|label|value

MinkContext Defines Steps...for querying the DOM

Pattern DescriptionThen /^I should see "(?P<text>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/

Checks that element with specified CSS contains specified text

Then /^the "(?P<element>[^"]*)" element should contain "(?P<value>(?:[^"]|\\")*)"$/

Checks that element with specified CSS contains specified HTML

Then /^I should see an? "(?P<element>[^"]*)" element$/ Checks that element with specified CSS exists on page

Then /^I should not see an? "(?P<element>[^"]*)" element$/ Checks that element with specified CSS doesn't exist on page

Then /^the "(?P<field>(?:[^"]|\\")*)" field should contain "(?P<value>(?:[^"]|\\")*)"$/

Checks that form field with specified id|name|label|value has specified value

Then /^the "(?P<field>(?:[^"]|\\")*)" field should not contain "(?P<value>(?:[^"]|\\")*)"$/

Checks that form field with specified id|name|label|value doesn't have specified value

Then /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox should be checked$/ Checks that checkbox with specified id|name|label|value is checked

Then /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox should not be checked$/ Checks that checkbox with specified id|name|label|value is unchecked

MinkContext Defines Steps...for examining responses

Pattern DescriptionThen /^I should be on "(?P<page>[^"]+)"$/ Checks that current page path is equal to specified

Then /^the url should match "(?P<pattern>(?:[^"]|\\")*)"$/ Checks that current page path matches pattern

Then /^the response status code should be (?P<code>\d+)$/ Checks that current page response status is equal to specified

Then /^I should see "(?P<text>(?:[^"]|\\")*)"$/ Checks that page contains specified text

Then /^I should not see "(?P<text>(?:[^"]|\\")*)"$/ Checks that page doesn't contain specified text

Then /^the response should contain "(?P<text>(?:[^"]|\\")*)"$/ Checks that HTML response contains specified string

Then /^the response should not contain "(?P<text>(?:[^"]|\\")*)"$/ Checks that HTML response doesn't contain specified string

Then /^print last response$/ Prints last response to console

Then /^show last response$/ Opens last response content in browser

Let's Execute Our Feature With Behat

$ app/console behat @AcmeDemoBundle --env=test

Feature: Contact form

In order to contact an email address

As a visitor

I need to be able to submit a contact form

Scenario: Successfully submit the contact form # contact.feature:6

Given I am on "/demo/contact" # FeatureContext::visit()

When I fill in "Email" with "user@example.com" # FeatureContext::fillField()

Form field with id|name|label|value "Email" not found

And I fill in "Message" with "Hello there!" # FeatureContext::fillField()

And I press "Send" # FeatureContext::pressButton()

Then I should see "Message sent!" # FeatureContext::assertPageContainsText()

1 scenario (1 failed)

5 steps (1 passed, 3 skipped, 1 failed)

A Note About Step Results...of which there are seven

● Success: a definition was found and executing it did not throw an Exception●Undefined: a definition couldn't be found; all subsequent steps will be skipped●Pending: the definition threw the special PendingException, which means you have

work to do; skip remaining steps● Failure: a definition throws an Exception; Behat will skip remaining steps and

terminate with exit status 1•Behat can easily use PHPUnit's assertion framework or you can roll your own● Skipped: steps which were never executed●Ambiguous: multiple definitions matched a step●Redundant: multiple definitions share the same pattern

Implement the Contact Form

Implement the Contact Form

Implement the Contact Form

Let's Try That Again

$ app/console behat @AcmeDemoBundle --env=test

Feature: Contact form

In order to contact an email address

As a visitor

I need to be able to submit a contact form

Scenario: Successfully submit the contact form # contact.feature:6

Given I am on "/demo/contact" # FeatureContext::visit()

When I fill in "Email" with "user@example.com" # FeatureContext::fillField()

And I fill in "Message" with "Hello there!" # FeatureContext::fillField()

And I press "Send" # FeatureContext::pressButton()

Then I should see "Message sent!" # FeatureContext::assertPageContainsText()

1 scenario (1 passed)

5 steps (5 passed)

What Else Can Mink Do?

● Provide a single API for browser behavior• HTTP authentication, cookies, headers, sessions• Page examination via XPath or CSS selectors• Page manipulation (e.g. complete forms, click, hover, drag-and-drop)● Existing drivers can be used interchangeably• Symfony2 test client – simulated request serving • Goutte – headless, PHP web scraper• Sahi – brower-control toolkit (necessary for JS)• PhantomJS – headless browser-control for Webkit•http://www.ryangrenz.com/2011/05/30/experiments-with-behat-part1-mink-sahi-phantomjs/

Thanks!

http://behat.org/

http://mink.behat.org/

http://github.com/Behat

Create a bundle

php app/console generate:bundle --namespace=Acme/TodoBundle

git clone https://github.com/jmikola/top-shelf-php.git

Questions?

Thanks for joining Top Shelf PHP!

http://symfony.com

http://github.com/kriswallsmith/assetic

http://behat.org