Symfony2
Web application framework
DC PHP, Aug 27th 2014
© David Buchmann, Liip AG
About David Buchmann
Twitter: @dbu
- Senior backend developper at Liip
- Symfony2 expert
- Maintainer of Symfony2 Content Management Framework (CMF)
- Other topics: Varnish, Elasticsearch, Vagrant
- Liip is a web agency in Switzerland.
Outline
- Philosophy
- Dependency Injection
- Core Components
- HttpFoundation
- Routing
- Twig
- Using Symfony2 Framework
- Start a project
- Create your first bundle
- Model, View, Controller
- Some more background on Symfony2
Symfony2
- HTTP framework
- Components: Over 20 high quality, cleanly separated libraries
- Each suitable for standalone use
- Full stack framework
- All components sensibly wired together
- Conventions for folder structure
- Bundles: Plugins that integrate into full stack
Composer
Dependency management reloaded
- Define dependencies of your application
- Libraries declare their dependencies
- Composer resolves dependencies and downloads all needed libraries
- Per project, not global in your system
- Integrates autoloading
- Simple to use, simple to publish libraries
You should too!
- Think separation of concern over this or that framework
- Write standalone libraries
- Write separate integration layer for frameworks
- RAD: Tied to framework
Dependeny Injection
Dependeny Injection
class MyClass
{
public function do()
{
global $variable;
echo $variable;
}
}
NO!
Dependeny Injection
class MyClass
{
public function do()
{
echo Registry::instance()->get('variable');
}
}
Meh. A bit better but not good.
Dependeny Injection
class MyClass
{
private $variable;
public function __construct($variable)
{
$this->variable = $variable;
}
public function do()
{
echo $this->variable;
}
}
This is dependency injection!
Dependeny Injection
- Independant of any "service container"
- Push instead of pull
- Dependencies are explicit
- Reusable code
- Testable code
Symfony2 Dependeny Injection
- "service": lazy loaded class instance
- Define what other services need to be injected
- Service container only instantiates when actually needed
Core Components:
HttpFoundation
HttpFoundation
HTTP object oriented
- Request
- Response
- Headers, Content negotiation
- File upload and download, streaming
- Session
The Request object
// /test-path?name=test
$request = Request::createFromGlobals();
$request->query->get('name'); // test
$request->getLanguages(); // ordered by priority
$request->getContent(); // body of request
Request::create('/test-path', 'GET', array('name' => 'test'));
Creating a response
$response = new Response('Success', Response::HTTP_OK,
array('content-type' => 'text/plain'));
$response->headers->setCookie('foo' => 'bar');
$response->setMaxAge(3600); // let client cache for 1 hour
$response->send();
Special types of responses
$response = new RedirectResponse('http://php.net');
$response = new Response('Not Found', Response::HTTP_NOT_FOUND);
$path = '/path/to/file.txt';
$response = new BinaryFileResponse($file);
// overwrite the filename
$response->setContentDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
'filename.txt'
);
Routing
URL to PHP method
- Route
- RouteCollection
- RequestContext
- UrlMatcher
- UrlGenerator
Routes
- Path, with placeholders: /article/{slug}
- Requirements: Regular expressions for placeholders, HTTP method and scheme
- Array of default values
Request Matching
$route = new Route('/articles/{slug}',
array('controller' => 'MyClass'));
$routes = new RouteCollection();
$routes->add('article', $route);
// convenient when using HttpFoundation:
$context = RequestContext::fromRequest(
Request::createFromGlobals());
$matcher = new UrlMatcher($routes, $context);
$parameters = $matcher->match('/articles/my');
// array('slug'=>'my', 'controller'=>'MyClass',
// '_route'=>'article')
//
// Your application knows how to call MyClass
Generate URL
$generator = new UrlGenerator($routes, $context);
$generator->generate('article', array(
'slug' => 'my',
));
// /article/my
$generator->generate('article', array(
'slug' => 'my'), true);
// http://example.com/article/my
Load routes from file
article:
path: /article/{slug}
defaults: { _controller: 'MyClass' }
requirements:
slug: [a-zA-Z\-]
// uses the optional Config component
$locator = new FileLocator(array(__DIR__));
$loader = new YamlFileLoader($locator);
$collection = $loader->load('routes.yml');
Twig
Templating language
- Clean separation of application logic and templating
- Secure: Escaping by default
- Convenient for frontend devs
- Much more readable than PHP templates
- Compiled to PHP, no speed penalty
- Write custom extensions in PHP
Twig in 3 bullet points
- {{ ... }} echo a value
- {% ... %} execute something
- provide control logic, loops and blocks
Example
<!DOCTYPE html>
<html>
<head>
<title>Welcome to Washington DC PHP!</title>
</head>
<body>
<h1>Hello: {{ name }}</h1>
</body>
</html>
Control and data access
<ul>
{% for user in users if user.active %}
<li>{{ user.username }}</li>
{% else %}
<li>No users found</li>
{% endfor %}
</ul>
- When object, isActive(), getActive() or public property active
- When array, index 'active'
Inheritance
{# base.html.twig #}
<html>
<head>
{% block title %}Liip AG{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
{% extends 'base.html.twig' %}
{% block title %}
{{ content.title }} - {{ parent() }}
{% endblock %}
{% block body %}{{ content.text }}{% endblock %}
Extension points
- Filter: {{ name|upper }}, {{ body|raw }}
- Function: {{ min(a, b, c) }}
- Tag: {% for i in 1..5 %}{{ i }}{% endfor %}
- Lots of useful built-in things
- Build your own: Filters and functions are easy
- Custom Tags: Render PHP code in your code
Symfony2 full stack framework
Setting up a project
$ curl -s https://getcomposer.org/installer | php
$ php composer.phar create-project symfony/framework-standard-edition my-project 2.5.*
Configure your web server to point to web subfolder of my-project
Directory Layout
- app/ application configuration, bootstrap and overwritten files
- src/ code specific to this application, organized as bundles
- vendor/ all third party code, managed by composer
- web/ web root, application entry point and static files
Develop
- Entry point is "hostname.lo/app.php"
- If you have mod_rewrite, there is a .htaccess to use "hostname.lo/"
- Debug entry point is /app_dev.php
- Templates and configuration checked for changes and automatically re-compiled
- Different configuration per environment
- Debug toolbar at the bottom
Create your first bundle
$ php app/console generate:bundle --namespace=DC/HelloBundle --format=yml
# simply accept all defaults, they are fine
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
// ...
new DC\HelloBundle\DCHelloBundle(),
);
return $bundles;
}
Also links routing file from app/config/routing.yml
Bundle Directory Layout
- Controller/ code for handling requests
- DependencyInjection/ for managing bundle configuration (advanced feature)
- Resources/config/ configuration files for this bundle, e.g. routing
- Resources/views/ template files
- Resources/public/ static files to expose through the web/ directory (stylesheets, javascript, ...)
- Tests/ phpunit tests of this bundle.
Controller
Controller
namespace DC\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
public function greetAction($name)
{
return new Response("Hello $name");
}
}
Routing: Map request to controller
- As seen before, pattern /hello/{name}
- Controller as DCHelloBundle:Hello:greet
- Symfony2 instantiates DC\HelloBundle\Controller\HelloController and calls greetAction
- Method parameters from request
View
Symfony2 twig integration
- Controller can easily render a template
$this->render(
'DCHelloBundle:Hello:greet.html.twig',
array('name' => $name)
);
- Convenience Twig functions, e.g. path() function to render a route
- Render a controller action from twig
Model
<?php
namespace DC\HelloBundle\Entity;
class Page
{
private $mainText;
public function __construct($text)
{
$this->mainText = $text;
}
public function getMainText()
{
return $this->mainText;
}
public function setMainText($text)
{
$this->mainText = $text;
}
}
Use it
Controller
$page = new \DC\HelloBundle\Entity\Page('test');
return $this->render(
'DCHelloBundle:Default:index.html.twig',
array('name' => $name, 'page' => $page)
);
Template
<p>{{ page.mainText }}</p>
Doctrine ORM
- Object-Relational mapper
- Any PHP objects, not active record
- Map with annotations, XML or yaml files
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Page
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="main_text", type="string")
*/
private $mainText;
...
Console commands to manage database
$ app/console doctrine:schema:update --dump-sql
CREATE TABLE Page (id INTEGER NOT NULL, main_text VARCHAR(255) NOT NULL, PRIMARY KEY(id))
$ app/console doctrine:schema:update --force
Updating database schema...
Database schema updated successfully! "1" queries were executed
Insert Doctrine Objects
$page = new Page('DCPHP in Washington');
$em = $this->getDoctrine()->getManager();
$em->persist($page);
$em->flush();
Read Doctrine Objects
$em = $this->getDoctrine()->getManager();
$repository = $em->getRepository('DCHelloBundle:Page')
$page = $repository->find($id);
if (! $page) {
throw new NotFoundHttpException("no page $id");
}
// render as before
}
Doctrine shell commands
- doctrine:mapping:info - informations about the entities you defined
- doctrine:generate:entities - generate classes from xml/yml mapping information
- doctrine:generate:entity - interactive shell to define entity
- doctrine:mapping:import - generate entities by inspecting an existing database
- doctrine:migrations:status|diff|migrate - manage database changes in change scripts
Reuse: There is a bundle for that!
Find existing code
- PHP has lots of cool libraries
- Bundles integrate them into Symfony2
- Fight the NIH syndrome
- Each bundle you can use is one less you need to write and maintain
- knpbundles.com
- packagist.org
Integrate KnpMarkdownBundle
php composer.phar require "knplabs/knp-markdown-bundle:1.2.*"
Add to app/AppKernel.php
$bundles = array(
// ...
new Knp\Bundle\MarkdownBundle\KnpMarkdownBundle(),
);
Use it in template
{{ page.mainText|markdown }}
Using git
- Ignore the vendor/ directory
- But track the composer.lock file (and composer.json of course)
- The standard edition provides a reasonable .gitignore file
- Track the parameters.yml.dist file but have parameters.yml ignored
Lots of interesting components
- Console: Run console commands
- Form: Build and handle web form submission
- DomCrawler and CssSelector: Web scraper and functional testing
- EventDispatcher: Register to events and define your own event types
- Process: Run commands in separate processes
- Validator: Rule format and validate whether data follows the rules
- Security: Handle security inside the application
Thank you!
Questions / Input / Feedback ?
@dbu