Symfony / Web Services - part 2.1: Creation bootstrap 21/01/2015
Deprecated: This series has been re-written - see The Ultimate Developer Guide to Symfony
This is the second article of the series on managing Web Services in a Symfony environment. Have a look at the first one: 1. Introduction.
In this post we'll create an empty application and prepare it:
- Installing the standard edition
- Twitching for tests
- Patching for JSON submit
- Setting up the authentication
- Conclusion
Installing the standard edition
First of all, we need to create an empty Symfony application:
composer create-project symfony/framework-standard-edition ws
Note: Take the time to configure a MySQL database, we'll need it later.
Next we'll configure an Apache's virtual host (should be in /etc/apache2/sites-available/ws.conf
):
<VirtualHost *:80>
ServerName ws.local
DocumentRoot /home/foobar/ws/web
ErrorLog "/home/foobar/ws/app/logs/apache_errors.log"
CustomLog "/home/foobar/ws/app/logs/apache_accesses.log" common
<Directory /home/foobar/ws/web>
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Require all granted
Order allow,deny
allow from all
</Directory>
</VirtualHost>
Apache will require access to the logs and cache directories, as well as your
user. The easiest way to do so is to change Apache's user and group to yours in
/etc/apache2/envvars
:
export APACHE_RUN_USER=foobar
export APACHE_RUN_GROUP=foobar
In order for this to work we'll update our /etc/hosts
file:
echo '127.0.0.1 ws.local' | sudo tee -a /etc/hosts
And finally we'll restart the web server:
sudo service apache2 restart
We should be able to see "Homepage" when browsing http://ws.local/app_dev.php/app/example
Let's commit our work:
git init
git add -A
git ci -m 'Created a standard Symfony application'
Twitching for tests
As explained in this article, we'll twitch the standard edition a little bit in order to make tests more explicit.
First we create a bootstraping file:
<?php
// File: app/bootstrap.php
require __DIR__.'/bootstrap.php.cache';
require __DIR__.'/AppKernel.php';
Then we configure PHPUnit to use it:
<?xml version="1.0" encoding="UTF-8"?>
<!-- http://phpunit.de/manual/4.1/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="bootstrap.php"
>
<testsuites>
<testsuite name="Project Test Suite">
<directory>../tests</directory>
</testsuite>
</testsuites>
</phpunit>
We've decided to put our tests in a separate tests
directory, allowing us to
decalre an autoload mapping specific for development. To fully optimize our
autoloading, we'll also define our src/AppBundle
folder as a path for the
AppBundle
namespace, using PSR-4:
{
"name": "symfony/framework-standard-edition",
"license": "MIT",
"type": "project",
"description": "The \"Symfony Standard Edition\" distribution",
"autoload": {
"psr-4": { "AppBundle\\": "src/AppBundle" }
},
"autoload-dev": {
"psr-4": { "AppBundle\\Tests\\": "tests" }
},
"require": {
"php": ">=5.3.3",
"symfony/symfony": "2.6.*",
"doctrine/orm": "~2.2,>=2.2.3",
"doctrine/doctrine-bundle": "~1.2",
"twig/extensions": "~1.0",
"symfony/assetic-bundle": "~2.3",
"symfony/swiftmailer-bundle": "~2.3",
"symfony/monolog-bundle": "~2.4",
"sensio/distribution-bundle": "~3.0.12",
"sensio/framework-extra-bundle": "~3.0",
"incenteev/composer-parameter-handler": "~2.0"
},
"require-dev": {
"sensio/generator-bundle": "~2.3"
},
"scripts": {
"post-root-package-install": [
"SymfonyStandard\\Composer::hookRootPackageInstall"
],
"post-install-cmd": [
"Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::removeSymfonyStandardFiles"
],
"post-update-cmd": [
"Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::removeSymfonyStandardFiles"
]
},
"config": {
"bin-dir": "bin"
},
"extra": {
"symfony-app-dir": "app",
"symfony-web-dir": "web",
"symfony-assets-install": "relative",
"incenteev-parameters": {
"file": "app/config/parameters.yml"
},
"branch-alias": {
"dev-master": "2.6-dev"
}
}
}
To make it official, we need to run the following command:
composer dump-autoload
We'll also install phpspec:
composer require phpspec/phpspec:~2.1
With this our tests will be awesome! Time to commit:
git add -A
git commit -m 'Configured tests'
Patching for JSON submit
Symfony provides the posted data in the Request
's request
attribute, except
if the content type is application/json
, as it will be our case. To fix this
behavior we'll follow the steps described in this article.
Let's start by the creation of an event listener:
<?php
// File: src/AppBundle/EventListener/SubmitJsonListener.php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
/**
* PHP does not populate $_POST with the data submitted via a JSON Request,
* causing an empty $request->request.
*
* This listener fixes this.
*/
class SubmitJsonListener
{
/**
* @param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$hasBeenSubmited = in_array($request->getMethod(), array('POST', 'PUT'), true);
$isJson = ('application/json' === $request->headers->get('Content-Type'));
if (!$hasBeenSubmited || !$isJson) {
return;
}
$data = json_decode($request->getContent(), true);
if (JSON_ERROR_NONE !== json_last_error()) {
$event->setResponse(new JsonResponse(array('error' => 'Invalid or malformed JSON'), 400));
}
$request->request->add($data ?: array());
}
}
Finally we'll register it in the Dependency Injection Container:
# File: app/config/services.yml
services:
app.submit_json_listener:
class: AppBundle\EventListener\SubmitJsonListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Setting up the authentication
HTTP basic authentication can be configured through the app/config/security.yml
file, as described in the official documentation.
In the end we should have something like this:
# app/config/security.yml
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
providers:
in_memory:
memory:
users:
spanish_inquisition:
password: 'NobodyExpectsIt!'
roles:
- ROLE_USER
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
default:
anonymous: ~
http_basic: ~
stateless: true
access_control:
- { path: /.*, roles: ROLE_USER }
Now to comply with our description we need to customize the error. We can do so using another event listener:
<?php
// File: src/AppBundle/EventListener/ForbiddenExceptionListener.php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
/**
* PHP does not populate $_POST with the data submitted via a JSON Request,
* causing an empty $request->request.
*
* This listener fixes this.
*/
class ForbiddenExceptionListener
{
/**
* @param GetResponseForExceptionEvent $event
*/
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
if (!$exception instanceof AccessDeniedException) {
return;
}
$error = 'The credentials are either missing or incorrect';
$event->setResponse(new JsonResponse(array('error' => $error), 403));
}
}
And to register it:
# File: app/config/services.yml
services:
app.submit_json_listener:
class: AppBundle\EventListener\SubmitJsonListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
app.forbidden_exception_listener:
class: AppBundle\EventListener\ForbiddenExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException, priority: 10 }
Note: the Symfony Security event listener has a priority set to 0. In order for our listener to be executed, we need to set a higher one, like 10.
As you can see by browsing http://ws.local/app_dev.php/app/example, we now need
to provide the spanish_inquisition
with the NobodyExpectsIt!
password to
access the page.
This is enough for today, we'll commit our work:
git add -A
git commit -m 'Created custom event listeners'
Conclusion
Our application is now ready!
In the [next article](/2015/01/28/sf-ws-part-2-2-creation-pragmatic.html we'll create the first endpoint, the creation of profiles, using a pragmatic approach.