Rebuilding our Foundation
-
Upload
jessica-mauerhan -
Category
Technology
-
view
178 -
download
0
Transcript of Rebuilding our Foundation
@jessicamauerhan | Madison PHP | https://joind.in/16022
Rebuilding Our Foundation
How We Used Symfony To Rewrite Our Application
@jessicamauerhan | Madison PHP | https://joind.in/16022
@jessicamauerhan | Madison PHP | https://joind.in/16022
● Project History ● Problems and Goals● Why Symfony?● Rapid Development
of Quality Code● Doctrine DBAL / ORM
Topics● Automated Testing● Sonata Admin Bundle● Creating an API with
Symfony● Dependency Injection● Current Project
Status
2
@jessicamauerhan | Madison PHP | https://joind.in/16022
● Learning Management System● Content Production● E-Commerce● Business to Business (B2B)
Application Summary
3
@jessicamauerhan | Madison PHP | https://joind.in/16022
Problematic History● Broken Admin Panel● No Documentation of Basic Processes● Frontend Site Worked, Progress Stalled● Complex Logic not Documented
4
@jessicamauerhan | Madison PHP | https://joind.in/16022
Business Goals● Add Missing Admin Panel Functionality● Add New Features Without Breaking Existing Features● Avoid Downtime
5
@jessicamauerhan | Madison PHP | https://joind.in/16022
Technical Goals● Maintainable Code● Quality Code● Documentation● Rapid Development● Easy Deployment● Zero Regressions Per Release
6
@jessicamauerhan | Madison PHP | https://joind.in/16022
Why Symfony?● Community
○ Third Party Code Integration○ Blazing Trails ○ Popularity○ Support
7
@jessicamauerhan | Madison PHP | https://joind.in/16022
Why Symfony?● Technology
○ Dependency Injection & Decoupling○ Unit Testing○ Functional Testing○ Behavior Testing
8
@jessicamauerhan | Madison PHP | https://joind.in/16022
Rapid Development of Quality Code● Version Control: Git● Development Workflow
9
@jessicamauerhan | Madison PHP | https://joind.in/16022
Image sourced from Atlassian: https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflowLicensed under the Creative Commons Attribution 2.5 Australia License.
10
@jessicamauerhan | Madison PHP | https://joind.in/16022
Rapid Development of Quality Code● Version Control: Git● Development Workflow: Gitflow● Code Style Guide: PSR-2
11
@jessicamauerhan | Madison PHP | https://joind.in/16022
Rapid Development of Quality Code● Version Control: Git● Development Workflow: Gitflow● Code Style Guide: PSR-2● Code Quality Rules
12
@jessicamauerhan | Madison PHP | https://joind.in/16022
PHP Mess Detector$ php composer require "phpmd/phpmd" --dev
$ php composer install --no-dev
13
@jessicamauerhan | Madison PHP | https://joind.in/16022
PHP Mess Detector Rules<?xml version="1.0"?><ruleset name="Code Quality" xmlns="http://pmd.sf.net/ruleset/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd" xsi:noNamespaceSchemaLocation=" http://pmd.sf.net/ruleset_xml_schema.xsd"> <description>Custom Code Quality Rules</description> <!--Rulesets: http://phpmd.org/rules/index.html--> <rule ref="rulesets/cleancode.xml"/> <rule ref="rulesets/naming.xml/ShortVariable"> <properties> <property name="minimum" value="4"/> </properties> </rule></ruleset>
14
@jessicamauerhan | Madison PHP | https://joind.in/16022
Rapid Development of Quality Code● Version Control● Development Workflow: Gitflow● Code Style Guide: PSR-2● Code Quality Rules: PHP Mess Detector● Code Quality Enforcement
15
@jessicamauerhan | Madison PHP | https://joind.in/16022
Git Pre-Commit Code Quality Hook#!/usr/bin/env php<?php
require __DIR__ . '/../../vendor/autoload.php';use Symfony\Component\Console\Application;
class CodeQualityTool extends Application{ private $projectRoot; const PHP_FILES_IN_SRC = '/^src\/(.*)(\.php)$/'; const PHP_FILES_IN_APPLICATION = '/^application\/(.*)(\.php)$/';
public function __construct() { $this->projectRoot = realpath(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR); parent::__construct('Code Quality Tool', '1.0.0'); }}
16
@jessicamauerhan | Madison PHP | https://joind.in/16022
Git Pre-Commit Code Quality Hookprivate function extractCommitedFiles(){ $files = []; $output = [];
exec("git diff --cached --name-status --diff-filter=ACM", $output); foreach ($output as $line) { $filename = trim(substr($line, 1)); $isAppFile = preg_match(self::PHP_FILES_IN_APPLICATION, $filename); $isSrcFile = preg_match(self::PHP_FILES_IN_SRC, $filename); if ($isAppFile || $isSrcFile) { $files[] = $filename; } } return $files;}
17
@jessicamauerhan | Madison PHP | https://joind.in/16022
Git Pre-Commit Code Quality Hookprivate function checkPhpMd($files){ $succeed = true; foreach ($files as $file) { $processArgs = ['bin/phpmd', $file, 'text', 'phpmd-rules.xml']; $processBuilder = new ProcessBuilder($processArgs); $processBuilder->setWorkingDirectory($this->projectRoot); $process = $processBuilder->getProcess(); $process->run(); if (!$process->isSuccessful()) { $this->output->writeln($file); $this->output->writeln($process->getErrorOutput()); $this->output->writeln($process->getOutput()); $succeed = false; } } return $succeed;}
18
@jessicamauerhan | Madison PHP | https://joind.in/16022
Git Pre-Commit Code Quality Hookpublic function doRun(InputInterface $input, OutputInterface $output) { $this->input = $input; $this->output = $output;
$output->writeln('<info>Fetching files</info>'); $files = $this->extractCommitedFiles();
$info = '<info>Checking for messy code with PHPMD</info>'; $output->writeln($info); if (!$this->checkPhpMd($files)) { throw new Exception(sprintf('There are PHPMD violations!')); }}
19
@jessicamauerhan | Madison PHP | https://joind.in/16022
Rapid Development of Quality Code● Version Control: Git● Development Workflow: Gitflow● Code Style Guide: PSR-2● Code Quality Rules: PHP Mess Detector● Code Quality Enforcement: Git Hooks
20
@jessicamauerhan | Madison PHP | https://joind.in/16022
Doctrine
21
@jessicamauerhan | Madison PHP | https://joind.in/16022
Doctrine DBAL / ORM● Database Abstraction Layer (DBAL)
○ Database Vendor Agnostic○ Query Builder
● Object Relational Mapper (ORM)○ Maps Objects to Tables, Properties to Fields○ Object Relationships○ DQL - it's like SQL for your Objects
22
@jessicamauerhan | Madison PHP | https://joind.in/16022
Generating Entity Classes (Annotations)$ php app/console doctrine:mapping:import --force AcmeBlogBundle xml
$ php app/console doctrine:mapping:convert annotation ./src
$ php app/console doctrine:generate:entities AcmeBlogBundle
23
@jessicamauerhan | Madison PHP | https://joind.in/16022
Annotated Entity Class<?phpnamespace Acme\BlogBundle\Entity;use Doctrine\ORM\Mapping as ORM;/*** Acme\BlogBundle\Entity\BlogComment** @ORM\Table(name="blog_comment")* @ORM\Entity*/class BlogComment{ /** * @var integer $id * @ORM\Column(name="id", type="bigint") * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ private $id;
/** * @var string $author * @ORM\Column(name="author", type="string", length=100, nullable=false) */ private $author;}
24
@jessicamauerhan | Madison PHP | https://joind.in/16022
Other Doctrine Tools● Doctrine Migrations● Data Fixtures Library
25
@jessicamauerhan | Madison PHP | https://joind.in/16022
Introducing Automated Tests
26
@jessicamauerhan | Madison PHP | https://joind.in/16022
$ php composer require "behat/behat" --dev
Installing Behat
27
@jessicamauerhan | Madison PHP | https://joind.in/16022
Writing Features for Existing Code● Write Feature● Run Test
○ Test Passes - Double Check○ Test Fails
■ Described Feature Wrong■ Mistake in Test Code■ Feature is Broken
28
@jessicamauerhan | Madison PHP | https://joind.in/16022
New Symfony Development
29
@jessicamauerhan | Madison PHP | https://joind.in/16022
Installing Symfony Framework
30
@jessicamauerhan | Madison PHP | https://joind.in/16022
Installing Symfony Framework
31
@jessicamauerhan | Madison PHP | https://joind.in/16022
Merging Symfony Framework with Existing Code
● Install Separately● Move Directories● Combine composer.json
○ scripts○ extra○ autoload
32
@jessicamauerhan | Madison PHP | https://joind.in/16022
Generate App Bundle
33
@jessicamauerhan | Madison PHP | https://joind.in/16022
Generate App BundleC:\wamp\www\test>php app\console generate:bundle Welcome to the Symfony2 bundle generator Your application code must be written in bundles. This command helps you generate them easily. Each bundle is hosted under a namespace (like Acme/Bundle/BlogBundle). The namespace should begin with a "vendor" name like your company name, your project name, or your client name, followed by one or more optional category sub-namespaces, and it should end with the bundle name itself (which must have Bundle as a suffix). See http://symfony.com/doc/current/cookbook/bundles/best_practices.html#index-1 for more details on bundle naming conventions. Use / instead of \ for the namespace delimiter to avoid any problem. Bundle namespace: Demo/AppBundle
34
@jessicamauerhan | Madison PHP | https://joind.in/16022
Generate App BundleIn your code, a bundle is often referenced by its name. It can be the concatenation of all namespace parts but it's really up to you to come up with a unique name (a good practice is to start with the vendor name). Based on the namespace, we suggest DemoAppBundle. Bundle name [DemoAppBundle]: AppBundle
35
@jessicamauerhan | Madison PHP | https://joind.in/16022
The bundle can be generated anywhere. The suggested default directory uses the standard conventions.
Target directory [C:\wamp\www\test/src]:
Determine the format to use for the generated configuration.
Configuration format (yml, xml, php, or annotation): annotation
To help you get started faster, the command can generate some code snippets for you.
Do you want to generate the whole directory structure [no]? yes
Generate App Bundle
36
@jessicamauerhan | Madison PHP | https://joind.in/16022
Generate App Bundle Summary before generation You are going to generate a "Demo\AppBundle\AppBundle" bundle in "C:\wamp\www\test/src/" using the "annotation" format. Do you confirm generation [yes]? Bundle generation Generating the bundle code: OK Checking that the bundle is autoloaded: OK Confirm automatic update of your Kernel [yes]? Enabling the bundle inside the Kernel: OK Confirm automatic update of the Routing [yes]? Importing the bundle routing resource: OK You can now start using the generated code!
37
@jessicamauerhan | Madison PHP | https://joind.in/16022
Sonata Admin
38
@jessicamauerhan | Madison PHP | https://joind.in/16022 39
@jessicamauerhan | Madison PHP | https://joind.in/16022 40
@jessicamauerhan | Madison PHP | https://joind.in/16022
$ php composer require "sonata-project/admin-bundle"
$ php composer require "sonata-project/doctrine-orm-admin-bundle"
Install Sonata Admin
41
@jessicamauerhan | Madison PHP | https://joind.in/16022
//AppKernel.phppublic function registerBundles(){ return [ //... new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Sonata\CoreBundle\SonataCoreBundle(), new Sonata\BlockBundle\SonataBlockBundle(), new Knp\Bundle\MenuBundle\KnpMenuBundle(), new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(), new Sonata\AdminBundle\SonataAdminBundle() ];}
Enable Sonata Admin Bundle
42
@jessicamauerhan | Madison PHP | https://joind.in/16022
Configure Sonata Admin Bundle#app\config.ymlsonata_block: default_contexts: [cms] blocks: sonata.admin.block.admin_list: contexts: [admin]
43
@jessicamauerhan | Madison PHP | https://joind.in/16022
#app\routing.ymladmin: resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml' prefix: /admin
_sonata_admin: resource: . type: sonata_admin prefix: /admin
Sonata Admin Bundle Routing
44
@jessicamauerhan | Madison PHP | https://joind.in/16022
$ php app/console assets:install web
$ php app/console cache:clear
Configure Sonata Admin Bundle
45
@jessicamauerhan | Madison PHP | https://joind.in/16022
<?php
namespace Demo\AppBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;use Sonata\AdminBundle\Form\FormMapper;use Sonata\AdminBundle\Datagrid\ListMapper;use Sonata\AdminBundle\Datagrid\DatagridMapper;
class CourseAdmin extends Admin{ protected function configureFormFields(FormMapper $formMapper){}
protected function configureListFields(ListMapper $listMapper){}
protected function configureDatagridFilters(DatagridMapper $datagridMapper){}}
Admin Class
46
@jessicamauerhan | Madison PHP | https://joind.in/16022
Admin Class - Create/Edit Form// Fields to be shown on create/edit formsprotected function configureFormFields(FormMapper $formMapper){ $formMapper ->add('title', 'text', ['label' => 'Course Title']) ->add('author', 'entity', ['class' => 'Demo\AppBundle\Entity\User']) ->add('description', null, ['required' => false]) ->add('categories') ->add('cost');}
47
@jessicamauerhan | Madison PHP | https://joind.in/16022
Admin Class - List// Fields to be shown on listsprotected function configureListFields(ListMapper $listMapper){ $listMapper ->addIdentifier('title') ->add('author') ->add('cost') ->add('categories');}
48
@jessicamauerhan | Madison PHP | https://joind.in/16022
Admin Class - Datagrid Filters// Fields to be shown on filter formsprotected function configureDatagridFilters(DatagridMapper $datagridMapper){ $datagridMapper ->add('title') ->add('author') ->add('categories');}
49
@jessicamauerhan | Madison PHP | https://joind.in/16022
Admin Service# Demo/AppBundle/Resources/config/admin.ymlservices: sonata.admin.course: class: Demo\AppBundle\Admin\CourseAdmin tags: - { name: sonata.admin, manager_type: orm, group: "Course Management", label: "Courses" } arguments: - ~ - Demo\AppBundle\Entity\Course - ~ calls: - [ setTranslationDomain, [AppBundle]]
50
@jessicamauerhan | Madison PHP | https://joind.in/16022
Admin Services - Add to Config# app/config/config.ymlimports: - { resource: parameters.yml } - { resource: security.yml } - { resource: services.yml } - { resource: @AppBundle/Resources/config/admin.yml }
51
@jessicamauerhan | Madison PHP | https://joind.in/16022
Done!
52
@jessicamauerhan | Madison PHP | https://joind.in/16022
Optimization
53
@jessicamauerhan | Madison PHP | https://joind.in/16022
Symfony Debug Toolbar
54
@jessicamauerhan | Madison PHP | https://joind.in/16022
Symfony Profiler
55
@jessicamauerhan | Madison PHP | https://joind.in/16022
Admin Class - Custom Query (List)public function createQuery($context = 'list'){ $queryBuilder = $this->getModelManager() ->getEntityManager('models\Course') ->createQueryBuilder();
$queryBuilder->select('course', 'categories') ->from('models\Course', 'course') ->leftJoin('course.categories', 'categories');
$proxyQuery = new ProxyQuery($queryBuilder); return $proxyQuery;}
56
@jessicamauerhan | Madison PHP | https://joind.in/16022
Admin Class - Custom Query (Edit) Page)public function getObject($id){ $dql = "SELECT course, categories FROM models\Course course LEFT JOIN course.categories categories WHERE course.id = :id";
$query = $this->getModelManager() ->getEntityManager('models\Course') ->createQuery($dql); $query->setParameter('id', $id);
$course = $query->getOneOrNullResult();
return $course;}
57
@jessicamauerhan | Madison PHP | https://joind.in/16022
API
58
@jessicamauerhan | Madison PHP | https://joind.in/16022
Install FOS Rest Bundle$ php composer require "friendsofsymfony/rest-bundle"
$ php composer require "jms/serializer-bundle"
$ php composer require "nelmio/api-doc-bundle"
59
@jessicamauerhan | Madison PHP | https://joind.in/16022
Enable FOS Rest Bundle and Serializer Bundle
//AppKernel.phppublic function registerBundles(){ return [ // ... new FOS\RestBundle\RestBundle(), new JMS\SerializerBundle\JMSSerializerBundle(), new Nelmio\ApiDocBundle\NelmioApiDocBundle() ];}
60
@jessicamauerhan | Madison PHP | https://joind.in/16022
Configuration# app/config/routing.ymlNelmioApiDocBundle: resource: "@NelmioApiDocBundle/Resources/config/routing.yml" prefix: /api/doc
# app/config/config.ymlnelmio_api_doc: ~framework: templating: engines: ['twig']
61
@jessicamauerhan | Madison PHP | https://joind.in/16022
API Controller<?php
namespace Demo\AppBundle\Controller;
use FOS\RestBundle\Controller\FOSRestController;use FOS\RestBundle\Controller\Annotations\Get;
class UsersController extends FOSRestController{ /** * @Get("/users/") */ public function getUsersAction() { $data = $this->getDoctrine()->getRepository('models\User')->findAll(); $view = $this->view($data, 200) ->setTemplate("AppBundle:Basic:json.twig") ->setTemplateVar('users'); return $this->handleView($view); }}
62
@jessicamauerhan | Madison PHP | https://joind.in/16022
JSON Twig Tpl{% spaceless %}{% if json is defined %} {{ json|json_encode()|raw }}{% else %} []{% endif %}{% endspaceless %}
63
@jessicamauerhan | Madison PHP | https://joind.in/16022
Add Doc Blocks with Annotation to API Controller<?php
namespace Demo\AppBundle\Controller;
use ...
class UsersController extends FOSRestController{ /** * @Get("/users/") * @ApiDoc( * resource=true, * description="List of Users", * output="models\User" * ) */ public function getUsersAction() { $data = $this->getDoctrine()->getRepository('models\User')->findAll(); $view = $this->view($data, 200) ->setTemplate("AppBundle:Basic:json.twig") ->setTemplateVar('users'); return $this->handleView($view); }}
64
@jessicamauerhan | Madison PHP | https://joind.in/16022
Dependency Injection
Service Location
Configure Dependencies
Outside of Class
Class Requests Dependencies
From Container
65
@jessicamauerhan | Madison PHP | https://joind.in/16022
Service Location<?php
namespace Demo\AppBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;use Symfony\Component\Console\Input\InputInterface;use Symfony\Component\Console\Output\OutputInterface;use Demo\AppBundle\Factory\MessageFactory;
class UserExportCommand extends ContainerAwareCommand{ protected function execute(InputInterface $input, OutputInterface $output) { $rootDir = $this->getContainer()->getParameter('kernel.root_dir'); $users = $this->getContainer()->getDoctrine()
->getRepository('models\User')->findAll();
$messageFactory = new MessageFactory(); foreach ($users as $user) { $emailMessage = $this->getMessageFactory()->generate($user); /** More Processing Logic Here */ } }}
66
@jessicamauerhan | Madison PHP | https://joind.in/16022
services: appbundle.message.factory: class: Demo\AppBundle\Factory\MessageFactory
Defining Services
67
@jessicamauerhan | Madison PHP | https://joind.in/16022
services: appbundle.message.factory: class: Demo\AppBundle\Factory\MessageFactory
appbundle.repositories.user: class: Demo\AppBundle\Entity\Repositories\UserRepository factory_service: doctrine.orm.entity_manager factory_method: getRepository arguments: - '\models\User'
Defining Services
68
@jessicamauerhan | Madison PHP | https://joind.in/16022
services: appbundle.message.factory: class: Demo\AppBundle\Factory\MessageFactory
appbundle.repositories.user: class: Demo\AppBundle\Entity\Repositories\UserRepository factory_service: doctrine.orm.entity_manager factory_method: getRepository arguments: - '\models\User'
appbundle.command.user_export: class: Demo\AppBundle\Command\UserExportCommand calls: - [ setMessageFactory, ["@appbundle.message.factory"]] - [ setUserRepository, ["@appbundle.repositories.user"]] - [ setRootDir, ["%kernel.root_dir%"]]
Defining Services
69
@jessicamauerhan | Madison PHP | https://joind.in/16022
Dependency Injection<?php
namespace Demo\AppBundle\Command;
use Symfony\Component\Console\Input\InputInterface;use Symfony\Component\Console\Output\OutputInterface;use Demo\AppBundle\Factory\MessageFactory;use Symfony\Component\Console\Command\Command;
class UserExportCommand extends Command{ protected $messageFactory; protected $rootDir; protected $userRepository;
}
70
@jessicamauerhan | Madison PHP | https://joind.in/16022
Dependency Injection<?php//..class UserExportCommand extends Command{ //..
public function setMessageFactory($messageFactory) { $this->messageFactory = $messageFactory; }
public function setRootDir($rootDir) { $this->rootDir = $rootDir; }
public function setUserRepository($userRepository) { $this->userRepository = $userRepository; }
71
@jessicamauerhan | Madison PHP | https://joind.in/16022
Dependency Injection<?php//..class UserExportCommand extends Command{ //..
public function getMessageFactory() { return $this->messageFactory; }
public function getRootDir() { return $this->rootDir; }
public function getUserRepository() { return $this->userRepository; }
72
@jessicamauerhan | Madison PHP | https://joind.in/16022
Dependency Injection
73
<?php
//..
class UserExportCommand extends Command{
//..
protected function execute(InputInterface $input, OutputInterface $output) { $users = $this->getUserRepository()->findAll(); /** Processing Logic */ foreach ($users as $user) { $emailMessage = $this->getMessageFactory()->generate($user); /** More Processing Logic Here */ } }}
@jessicamauerhan | Madison PHP | https://joind.in/16022
Controller Using Service Location
74
<?php
namespace Demo\AppBundle\Controller;
use FOS\RestBundle\Controller\FOSRestController;use FOS\RestBundle\Controller\Annotations\Get;
class UsersController extends FOSRestController{ /** * @Get("/users/") */ public function getUsersAction() { $data = $this->getDoctrine()->getRepository('models\User')->findAll(); $view = $this->view($data, 200) ->setTemplate("AppBundle:Basic:json.twig") ->setTemplateVar('users'); return $this->handleView($view); }}
@jessicamauerhan | Madison PHP | https://joind.in/16022
services: appbundle.controllers.users_controller: class: Demo\AppBundle\Controller\UsersController calls: - [ setUserRepository, ["@appbundle.repositories.user"]]
Defining A Controller As A Service
75
@jessicamauerhan | Madison PHP | https://joind.in/16022
Controller using Dependency Injection
76
/**
* @Route(service="appbundle.controllers.users_controller")
*/
class UsersController extends FOSRestController{ protected $userRepository;
public function setUserRepository($userRepository) { $this->userRepository = $userRepository; }
/** * @Get("/users/") */ public function getUsersAction() { $data = $this->userRepository->findAll(); $view = $this->view($data, 200) ->setTemplate("AppBundle:Basic:json.twig") ->setTemplateVar('users'); return $this->handleView($view); }}
@jessicamauerhan | Madison PHP | https://joind.in/16022
Current Project StatusIt's stable!
● New Symfony based Admin was launched after about 9 months (thanks developers!)
● Had a few bugs, took about 3 more months to be stable
● Over past year, close to 0 regressions (Thanks Behat!)
● Very limited downtime (Thanks Amazon, Elastic Beanstalk, Aurora!)
77
@jessicamauerhan | Madison PHP | https://joind.in/16022
Technical GoalsHappy Developers!
● Maintainable Code: Check
● Quality Code: Check
● Documentation: Check
● Rapid Development: Oh Yeah
● Easy Deployment: So Easy!
● Zero Regressions: Close Enough!
78
@jessicamauerhan | Madison PHP | https://joind.in/16022
Rebuilding Our Foundation
How We Used Symfony To Rewrite Our Application
@jessicamauerhan | Madison PHP | https://joind.in/16022
@jessicamauerhan | Madison PHP | https://joind.in/16022
ResourcesGitflow: https://github.com/nvie/gitflow
PSR-2: http://www.php-fig.org/psr/psr-2/
PHPMD: http://phpmd.org/
Pre-commit hook: https://gist.github.com/jmauerhan/d18e7c232acb3986134d
Doctrine: http://www.doctrine-project.org/
Doctrine Migrations Bundle: http://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html
Doctrine Data Fixtures Bundle: http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html
Behat: http://docs.behat.org/en/v2.5/
XDebug Wizard: http://xdebug.org/wizard.php
Sonata Project: https://sonata-project.org/bundles/admin/2-3/doc/index.html
FOS Rest Bundle: http://symfony.com/doc/master/bundles/FOSRestBundle/index.html
Nelmio API Doc Bundle: https://github.com/nelmio/NelmioApiDocBundle/blob/master/Resources/doc/index.md
80