The format – Talks then Hackingassets.en.oreilly.com/1/event/61/Top Shelf PHP Presentation...
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 "[email protected]"
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 "[email protected]" # 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 "[email protected]" # 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