Symfony MD Flavour Edition
Custom edition of Symfony2 project skeleton with highly opinionated structure changes., (*1)
Using
You can setup the project quickly via Composer:, (*2)
$ composer create-project michaldudek/symfony-project -s dev ./path/to/your/project
At the moment there isn't any generator/installer available which would automatically build your full application
structure so right after cloning the project some manual adjustments are required., (*3)
- Create
config/parameters.yml
by copying config/parameters.yml.dist
and adjust it
- Setup application PHP code:
- Rename
src/Project
to your application/project name - this will be your top level namespace [ProjectNamespace]
- Rename
src/Project/ProjectApp.php
to your application/project name - this will be your application class
[Project]App
- it is prefered to include a *App
suffix.
- Edit the new
src/[ProjectNamespace]/[Project]App.php
:
- Update its namespace on line 2
- Update its author on line 9 (good practice to include all developers working on the project here)
- Update its class name on line 11
- Edit
src/[ProjectNamespace]/Welcome/Controller/Home.php
and update the namespace on line 2.
- Edit
src/[ProjectNamespace]/Resources/services/services.yml
and update the controller namespace on line 9.
-
web/app.php
: On line 5 change the referenced class name to your project class
-
web/app_dev.php
: On line 14 change the referenced class name to your project class
-
console
: On line 6 change the referenced class name to your project class
-
Vagrantfile
:
- On line 1 set your application name.
- On line 23 you can adjust the VM IP to something unique
If you wish to use Capistrano for deployments:, (*4)
-
config/build/deploy.rb
:
- On line 4 set your application name
- On line 6 set your git repository URL
-
config/build/stages/dev.rb
:
- On line 4 configure your server
- On line 5 configure your deploy location
Now a $ vagrant up
command should bring up your Vagrant VM up and make it available under www.[APP_NAME].dev
domain., (*5)
Philosophy
The main philosophy behind the proposed changes, structure and rules is separation of concerns and inversion of
control. This means that each file, folder or "package" should only be concerned with a single purpose (of varied
scope of course) and it should not know about its execution context, instead being controled by an outside entity., (*6)
Think .cache
directory which only stores cache or src
directory which only stores the backend application code.
Think tests
directory which only stores testing code or views
directory which only stores view templates., (*7)
Changes
Directory and File Structure
The directory structure is quite different than that suggested by Symfony2 (or 3). The main difference is that there
is no ./app
directory. The idea is that all application code (including configuration) should be located inside
./src
and all "artifacts" (cache files, log files, dynamic parameter files) should live outside., (*8)
.cache/ # stores all auto-generated cache files
config/ # various configuration files which are not related to app structure
build/ # build configuration including asset packaging and deployments
assets/ # asset packaging configuration (see further down the README)
js.json
less.json
deploy/ # deployment configuration (see further down the README)
parameters.yml # git ignored file that should include environment specific configuration
logs/ # logs directory
resources/ # various project resources can be stored here, including docs or code coverage
src/ # backend application PHP code
[ProjectNamespace]/ # top level namespace of your application (typically app name)
Resources/ # Symfony2 related configuration files
config/ # configuration files
config.yml
config_*.yml
routing/ # routing files
routing.yml
routing_dev.yml
services/ # dependency injection container configuration files
services.yml
[Project]App.php # main application file that registers bundles
autoload.php
tests/ # contains all tests
vendor/ # composer dependencies
views/ # Twig view templates
web/ # publicly accessible directory
assets/ # contains dynamically built frontend assets (see further down the README)
components/ # Bower components location
js/ # frontend JavaScript
less/ # frontend CSS LESS source files
app.php # prod env front controller
app_dev.php # dev env front controller
.BUILD # contains the current build version
console # console file (used to be app/console)
Gulpfile # Gulpfile for building frontend
Makefile # Makefile that contains various common tasks
phpunit.xml.dist # default PHPUnit configuration
The above list omits some other files that are not strictly Symfony related, but rather ease other aspected of web app
development or add other functionalities or handle frontend, etc., (*9)
No Bundles, Not Even AppBundle
Current Symfony2 best practice is to have a single application bundle,
preferably called AppBundle
in which all your application logic resides. MD\Flavour goes a step further and doesn't
even require any such bundle. The idea is that your code should not be tied to something so framework specific as
"a bundle" and even having the word "Bundle" in your PHP namespace might feel uncomfortable., (*10)
In MD\Flavour your application is just that - an application built using PHP classes that heavily use dependency
injection pattern and therefore are not bound to any specific framework. The application flow can then be configured
with outside configuration files (think services.yml
or routing.yml
)., (*11)
The only two Symfony2 specific places are ./src/Project/ProjectApp.php
file in which you configure the Symfony bundles
that you want to use (AppKernel.php
in the standard edition) and ./src/Project/Resources/
directory in which various
configuration files are located., (*12)
Annotations are Forbidden!
The above also partially explains the fact that MD\Flavour strictly advises not to use annotations configuration in your
PHP code. While we understand that there is no performance difference between using annotations and e.g. YML files for
configuration, we believe that using annotations is tightly coupling your code with a specific framework or ORM., (*13)
An annotation requires to include an appropriate use
statement in the head of your PHP file which may trigger errors
in various QA analyses as well as potentially autoload those classes (which then need to be present)., (*14)
Ideally a PHP class should not know the context in which it is used. While convenient, using annotations does set that
context in the code. This may lead to readability and debug problems and most importantly can introduce issues when
switching context from e.g. Symfony2 to some other framework., (*15)
Controllers as Services
Clean separation is also another reason why so called "controllers" should be considered nothing more than just service
classes that need to be built with dependency injection. In many frameworks, including Symfony2, controllers are somehow
treated in a special way. They are allowed to have the dependency injection container injected to them, they are allowed
using hidden dependencies through that container and they are allowed to configure themselves e.g. by using annotations., (*16)
We propose that controllers are the same level citizen as any other PHP class. The main advantage of this is ability to
unit test them. With clear dependencies it is very easy to mock all required services and test the controller logic., (*17)
Refer to Symfony2 Cookbook in order to read more
about this approach, but in summary the only two things you need to do is register your controller as a service:, (*18)
# services.yml
services:
hello.controller:
class: Project\Controller\HelloController
arguments:
- @world
and refer to it in your route definition:, (*19)
# routing.yml
hello:
path: /
defaults: { _controller: hello.controller:hello }
Note that argument mapping from route parameters to method arguments still works using this way., (*20)
Controller Results
Taking the above rule one step further we propose that controllers should not know in what context their response should
be used. For easy reusability they shouldn't specifically return HTML or JSON responses. They should just return a data
result which then can be wrapped in appropriate format by a different party., (*21)
This means that the same controller could be used in normal HTTP request to return an HTML page rendered with Twig,
but when called with XHR they could return JSON formated data, while when a ?callback=jsonp
query parameter was sent
they can be wrapped in a JSONP response. These are just common examples and there obviously are many more
possibilities, but the core of it is that it's not up to the controller to decide what type of response it should give., (*22)
To allow and ease this mechanic, MD\Flavour package offers MD\Flavour\Controller\ResultConverter
class
(md.controller.result_converter
service) which converts a return value from a controller to a Response
object.
That return value can typically be an array, but in cases where the controller also wants to set the response's HTTP
status code it can be an instance of MD\Flavour\Controller\Result
class which takes an array and a status code as two
constructor arguments., (*23)
# HelloController.php
public function hello()
{
return [
'hello' => 'world'
];
}
is the same as, (*24)
# HelloController.php
use MD\Flavour\Controller\Result
public function hello()
{
return new Result([
'hello' => 'world'
], 200);
}
Additionally, the ResultConverter
will attempt to autoload a template to be rendered based on the controller name,
by removing the controller
word from it and changing all .
's and :
's to dir separators /
, e.g.
home.controller:index -> home/index.html.twig
or projects.list.controller:delete -> projects/list/delete.html.twig
., (*25)
The template to render for the route path can also be set with _template
attribute., (*26)
One URL per Controller Rule
Another controller rule that MD\Flavour proposes is keeping the number of mappings from a URL route to a controller
class to a minimum, preferably 1. A single URL path should be mapped to a single controller class with the controller
methods handling different HTTP methods., (*27)
An exception to the rule are "resource" routes with CRULD (Create-Read-Update-List-Delete) operations where an id
parameter is optional. The proposal is then to map them like this:, (*28)
GET /resource_url -> resource.controller:index
GET /resource_url/{id} -> resource.controller:show
POST /resource_url -> resource.controller:create
PUT /resource_url/{id} -> resource.controller:update
DELETE /resource_url/{id} -> resource.controller:delete
Bye Bye Doctrine, Hello Knit
Because of past issues and rough history with Doctrine2, MD\Flavour drops this ORM in favour of custom made simple
data mapper that follows a repository pattern and abstracts away communication with a database of your choice., (*29)
Visit Knit and KnitBundle for more
information., (*30)
Let's just say that using it is as simple as registering a repository service (which Knit forces as a best practice) and
injecting it wherever you please:, (*31)
# services.yml
services:
user_repository:
parent: knit.repository # handles repository instantiation
arguments:
- Project\Entity\User # managed object class name
- "users" # collection / table name in your database
Let Frontend Manage Frontend - No Assetic
In MD\Flavour opinion, the backend should be managed by the backend and the frontend should be managed by the frontend.
These two should be kept separate and building frontend assets (compiling CSS, JS, optimizing images, etc) should be
done with tools that are designed for it and with which frontend developers feel more comfortable. Therefore there is
no need for Assetic
., (*32)
Frontend Additions
While not particularly Symfony or MD\Flavour related, this project skeleton also adds several tools and makes some
recommendations on how to manage the frontend., (*33)
Bower Components
It is prefered to use Bower as the asset manager. To be accessible by the web, all Bower components are downloaded to
./web/components/
directory. Please refer to Bower for information on how to use it., (*34)
Building JS & CSS Assets
This project introduces quite complex but powerful build system for frontend assets. At its core it uses Gulp and some
popular gulp plugins for minification, concatenation, generating sourcemaps, etc. But the configuration of it is quite
different., (*35)
If you look into ./config/build/assets/
directory you will find two files: js.json
and less.json
. Their contents
define and configure so called "packages" into which the assets should be compiled on build time., (*36)
The idea is that while concatenation is a great way to speed up page loading, having a single huge file to download
might sometimes be slower than simultaneously downloading two or three smaller files. This also helps a lot with
browser caching. Therefore, the two JSON files define "packages" that should be built., (*37)
The top level key is the package name and the generated file will be of the same name and its two properties define
which files should be included in the package (files
) and what files should be watched by Gulp when its watching for
changes in a given package (watch
). The watch
parameter is optional and will default to what files
is set to,
but it's especially useful when only one file that imports all other is compiled, but all the imported files still need
to be watched, e.g.:, (*38)
{
"lib": {
"files": "web/less/lib.less"
},
"app": {
"files": "web/less/page.less",
"watch": "web/less/**/*.less"
}
}
To build the assets simply run $ gulp
in the terminal or $ gulp watch
to watch the files. Also $ gulp js
and
$ gulp less
are available. Or, preferably, use the Makefile
., (*39)
Development
Several tools and concepts are introduced specifically for development process., (*40)
Makefile
Because various dev and build processes might be complex and require to run several commands in proper order or with
specific arguments, a Makefile
is a very useful tool to standardize and simplify all this., (*41)
The Makefile
included in this project contains several useful tasks, such as:, (*42)
make install # installs all dependencies required in production
make install_dev # installs all dependencies required in development
make assets # builds all frontend assets
make watch # watches the frontend assets for changes and compiles if necessary
make test # runs all tests
make lint # lints all the code
make qa # runs all registered Quality Assurance checks
make report # creates a report on the code (e.g. code coverage report)
See $ make help
for list of all high-level tasks and view Makefile for specifics., (*43)
It is highly encouraged to wrap all commands inside the Makefile
and advise all developers involved in the project
to use it., (*44)
The BUILD Version
MD\Flavour introduces a concept of a build version. It is most useful when referencing dynamically built frontend
assets inside Twig templates. Because the project's .htaccess file adds heavy caching to almost all
frontend files, CSS and JavaScript files are built with a build version appended to their name, e.g.
lib-2015100801.js
. That build version is read from .BUILD
file located in the root dir of the project (git ignored),
and if it doesn't exist or is empty, it defaults to dev
., (*45)
This .BUILD
file should be written by whatever deployment tool you are using (in our case - Capistrano) with whatever
version tag. In case of Capistrano this is a release timestamp, but it can be anything else, a release number or git
commit hash or a UNIX timestamp., (*46)
It can be referenced in PHP code using %kernel.build%
container parameter, {{ kernel.build }}
variable in Twig or
[Project]\[Project]App::getBuild()
method., (*47)
An example reference to a frontend file can look like this:, (*48)
<link rel="stylesheet" href="/assets/css/lib-{{ kernel.build }}.css">
Vagrant
A default Vagrantfile
is included in this project to build a basic Vagrant VM with PHP 5.6 and MySQL 5.6 as well as
configured Apache2 for immediate development. Upon installing the project you should be safe to run $ vagrant up
and
get the machine running. Please refer to ./resources/docs/Vagrant.md file for more docs
on Vagrant configuration., (*49)
Quality Assurance
We believe that Quality Assurance should be part of the development process from the very beginning. It's much easier to
keep high code standards straight on rather than go back and fix issues. Therefore the project is set with several tools
to ease QA checks from the get go., (*50)
To run all these checks simply run $ make qa
in your terminal. This will execute several tasks, including:, (*51)
-
PHPUnit - which obviously runs all PHP unit tests located in
./tests/
directory and which can be configured in
./phpunit.xml.dist
file;
-
PHPCS - PHP_CodeSniffer which checks for proper coding style
standards (tip: run
$ make phpcs_fix
to automatically fix what can be fixed) and which can be adjusted and configured
in ./phpcs.xml
file;
-
PHPMD - PHP Mess Detector which does static analysis of the PHP code and looks for common
bad practice, caveats, errors, etc. While this can be a very annoying tool if you're just starting with it, it proved
to be invaluable in keeping code standards really high. It can be configured in
./phpmd.xml
file.
-
JSHint - JSHint which runs static code analysis on JavaScript code located in
./web/js/
directory and can be configured in ./.jshintrc
.
Deployments
TBD., (*52)
Capistrano
TBD., (*53)
Chef Knife
TBD., (*54)
Other
TBD., (*55)
GitLab CI
TBD., (*56)