How to Setup Docker for Your Symfony Project
As you probably know, I am a big Symfony fan :) In the last few years I used Vagrant to set up my Symfony development environment more or less as described here. But we now have Docker and it’s time to move forward with a new setup. Docker containers are generally more lightweight than Vagrant virtual machines, so starting and stopping them is extremely fast. They also take a lot less disk space.
To setup a Docker container you need a Dockerfile
file and to combine several containers we use the docker-compose.yml
file. For our environment we will need two containers, one with the latest Ubuntu LTS to host the web server and our project files and one for MySQL.
The first container is defined in the Dockerfile
file as below:
FROM ubuntu:16.04 ADD . /app RUN apt-get update RUN apt-get install -y php apache2 libapache2-mod-php7.0 php-mysql php-intl git git-core curl php-curl php-xml composer zip unzip php-zip # Configure Apache RUN rm -rf /var/www/* \ && a2enmod rewrite \ && echo "ServerName localhost" >> /etc/apache2/apache2.conf ADD vhost.conf /etc/apache2/sites-available/000-default.conf # Install Symfony RUN mkdir -p /usr/local/bin RUN curl -LsS https://symfony.com/installer -o /usr/local/bin/symfony RUN chmod a+x /usr/local/bin/symfony # Add main start script for when image launches ADD run.sh /run.sh RUN chmod 0755 /run.sh WORKDIR /app EXPOSE 80 CMD ["/run.sh"]
Add this file in a new folder. We also need the vhost.conf
and run.sh
files used in the code from above.
The vhost.conf
file is used to configure the apache virtual host:
<VirtualHost *:80> ServerName localhost DocumentRoot /app/my_project/web <Directory /app/my_project/web> Options -Indexes AllowOverride All Require all granted </Directory> </VirtualHost>
The run.sh
file runs when the container starts and just makes sure that the cache
and logs
folders are set up before launching the apache web server:
#!/bin/bash mkdir -p my_project/app/cache my_project/app/logs touch my_project/app/logs/prod.log touch my_project/app/logs/dev.log chgrp -R www-data . chmod -R g+w my_project/app/cache my_project/app/logs source /etc/apache2/envvars tail -F /var/log/apache2/* my_project/app/logs/prod.log my_project/app/logs/dev.log & exec apache2 -D FOREGROUND
That’s all for our main Ubuntu container, we now need to create the docker-compose.yml
file:
version: "2" services: mysql: image: mysql:5.7 container_name: mysqldb ports: - "4000:3306" expose: - "3306" environment: MYSQL_ROOT_PASSWORD: yourrootpassword site: build: . container_name: myproject ports: - "8080:80" expose: - "80" depends_on: - mysql volumes: - .:/app
This will tell Docker to first start an MySQL 5.7 container, then our Ubuntu container that will be able to access the MySQL container using the mysql
host name.
Start everything with the docker-compose up
command.
When it’s done, open a new terminal (let the latest docker compose command run, do not stop it) and use the docker ps
command to see the running containers:
Now, you can take the id
of the Ubuntu container and ssh into it:
docker exec -t -i [ID] bash
Here you will start a new Symfony project as always (you will have to delete the my_project
folder created by Docker first):
rm -rf my_project symfony new my_project 2.8
If you name you project something else (yes, you will probably do that) just replace the all my_folder
occurrences in the files and commands above with the actual name of your project.
After you created the new Symfony project, it is better to rebuild the Docker containers so the setup of the cache
and logs
folders will be as intended. In the terminal window where you launched Docker, press Ctrl+C
to stop it then run docker-compose up
again.
That’s it! You can now access your new Symfony project using the following URL: http://localhost:8080/. To connect to MySQL you will use the mysql host name:
parameters: database_host: mysql database_port: null database_name: symfony database_user: root database_password: yourrootpassword
Thank you! Please let me know in the comments if you have any suggestions to improve this setup or problems running it.
How to Get Nearby Locations from MySQL Database
If you have a MySQL table with locations (latitude
and longitude
columns) you can get a list of locations near a specific point using the following query:
SELECT *, ((ACOS(SIN(<latitude> * PI() / 180) * SIN(latitude * PI() / 180) + COS(<latitude> * PI() / 180) * COS(latitude * PI() / 180) * COS((<longitude> - longitude) * PI() / 180)) * 180 / PI()) * 60 * 1.1515 * 1.609344) as distance FROM locations HAVING distance <= <distance> ORDER BY distance ASC
In this query we have noted the latitude of the reference point with <latitude>
, its longitude with <longitude>
and the maximum distance to search for with <distance>
(in kilometers).
To make this calculate the distance in miles, just remove the * 1.609344 from the end of the calculation:
SELECT *, ((ACOS(SIN(<latitude> * PI() / 180) * SIN(latitude * PI() / 180) + COS(<latitude> * PI() / 180) * COS(latitude * PI() / 180) * COS((<longitude> - longitude) * PI() / 180)) * 180 / PI()) * 60 * 1.1515) as distance FROM locations HAVING distance <= <distance> ORDER BY distance ASC
You can test the results using Google Maps. To measure distances in Google Maps you need to right-click and choose “Measure distance” from the popup menu.
How to Get Nearby Locations in Symfony with Doctrine
If you’re like me and use Symfony a lot, here’s how you can add a repository method for your entity to return objects ordered by distance from a specific location:
<?php namespace AppBundle\Entity; class LocationRepository extends \Doctrine\ORM\EntityRepository { public function findClosest($lat, $lng) { $qb = $this->createQueryBuilder('l'); $qb->addSelect('((ACOS(SIN(:lat * PI() / 180) * SIN(l.latitude * PI() / 180) + COS(:lat * PI() / 180) * COS(l.latitude * PI() / 180) * COS((:lng - l.longitude) * PI() / 180)) * 180 / PI()) * 60 * 1.1515) as HIDDEN distance'); $qb->orderBy('distance'); $qb->setParameter('lat', $lat); $qb->setParameter('lng', $lng); return $qb->getQuery()->getResult(); } }
To make this work we need to add the ACOS
, SIN
, COS
and PI
functions to Doctrine’s DQL as it does not have them by default.
First create 4 new classes in a new AppBundle/DQL
folder, one for each function we need to define.
AppBundle/DQL/Acos.php
<?php namespace ApiBundle\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode, Doctrine\ORM\Query\Lexer; class Acos extends FunctionNode { public $arithmeticExpression; public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) { return 'ACOS(' . $sqlWalker->walkSimpleArithmeticExpression( $this->arithmeticExpression ) . ')'; } public function parse(\Doctrine\ORM\Query\Parser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $this->arithmeticExpression = $parser->SimpleArithmeticExpression(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } }
AppBundle/DQL/Sin.php
<?php namespace ApiBundle\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode, Doctrine\ORM\Query\Lexer; class Sin extends FunctionNode { public $arithmeticExpression; public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) { return 'SIN(' . $sqlWalker->walkSimpleArithmeticExpression( $this->arithmeticExpression ) . ')'; } public function parse(\Doctrine\ORM\Query\Parser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $this->arithmeticExpression = $parser->SimpleArithmeticExpression(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } }
AppBundle/DQL/Cos.php
<?php namespace ApiBundle\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode, Doctrine\ORM\Query\Lexer; class Cos extends FunctionNode { public $arithmeticExpression; public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) { return 'COS(' . $sqlWalker->walkSimpleArithmeticExpression( $this->arithmeticExpression ) . ')'; } public function parse(\Doctrine\ORM\Query\Parser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $this->arithmeticExpression = $parser->SimpleArithmeticExpression(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } }
AppBundle/DQL/Pi.php
<?php namespace ApiBundle\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode, Doctrine\ORM\Query\Lexer; class Pi extends FunctionNode { public $arithmeticExpression; public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) { return 'PI()'; } public function parse(\Doctrine\ORM\Query\Parser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } }
The next and the last step is to add the above definitions to your app’s config.yml
file:
doctrine: dbal: # ... orm: auto_generate_proxy_classes: '%kernel.debug%' naming_strategy: doctrine.orm.naming_strategy.underscore auto_mapping: true dql: numeric_functions: acos: ApiBundle\DQL\Acos sin: ApiBundle\DQL\Sin cos: ApiBundle\DQL\Cos pi: ApiBundle\DQL\Pi
That’s it. Now it will all work as expected. If you need to add more specific functions to DQL, just look at the above classes and make your own.
Symfony OAuth Authentication for Your Mobile Application
Let’s say you built an API using Symfony and you need to access it from a mobile application using authenticated requests on behalf of your users.
Here’s how to make this work using Symfony 2.8 and Doctrine.
Install FOSOAuthServerBundle
We will use the FOSOAuthServerBundle to implement this feature. Install it using the following command:
composer require friendsofsymfony/oauth-server-bundle
Next, enable the bundle in the AppKernel.php
file:
public function registerBundles() { $bundles = array( // ... new FOS\OAuthServerBundle\FOSOAuthServerBundle(), ); }
Create OAuth model classes
To create the OAuth model classes just add the following files to your project. Here we already have FOSUserBundle installed and set up to use the ApiBundle\Entity\User
class.
src/ApiBundle/Entity/Client.php
<?php namespace ApiBundle\Entity; use FOS\OAuthServerBundle\Entity\Client as BaseClient; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Client extends BaseClient { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; public function __construct() { parent::__construct(); // your own logic } }
src/ApiBundle/Entity/AccessToken.php
<?php namespace ApiBundle\Entity; use FOS\OAuthServerBundle\Entity\AccessToken as BaseAccessToken; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class AccessToken extends BaseAccessToken { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\ManyToOne(targetEntity="Client") * @ORM\JoinColumn(nullable=false) */ protected $client; /** * @ORM\ManyToOne(targetEntity="User") */ protected $user; }
src/ApiBundle/Entity/RefreshToken.php
<?php namespace ApiBundle\Entity; use FOS\OAuthServerBundle\Entity\RefreshToken as BaseRefreshToken; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class RefreshToken extends BaseRefreshToken { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\ManyToOne(targetEntity="Client") * @ORM\JoinColumn(nullable=false) */ protected $client; /** * @ORM\ManyToOne(targetEntity="User") */ protected $user; }
src/ApiBundle/Entity/AuthCode.php
<?php namespace ApiBundle\Entity; use FOS\OAuthServerBundle\Entity\AuthCode as BaseAuthCode; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class AuthCode extends BaseAuthCode { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\ManyToOne(targetEntity="Client") * @ORM\JoinColumn(nullable=false) */ protected $client; /** * @ORM\ManyToOne(targetEntity="User") */ protected $user; }
Configure FOSOAuthServerBundle
Import the routing configuration in your app/config/routing.yml
file:
fos_oauth_server_token: resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml" fos_oauth_server_authorize: resource: "@FOSOAuthServerBundle/Resources/config/routing/authorize.xml"
Add FOSOAuthServerBundle settings in app/config/config.yml
:
fos_oauth_server: db_driver: orm # Drivers available: orm, mongodb, or propel client_class: ApiBundle\Entity\Client access_token_class: ApiBundle\Entity\AccessToken refresh_token_class: ApiBundle\Entity\RefreshToken auth_code_class: ApiBundle\Entity\AuthCode service: user_provider: fos_user.user_provider.username
Back to the models
Generate a migration and migrate the database:
php app/console doctrine:migrations:diff php app/console doctrine:migrations:migrate
…or, if you’re not using migrations, just update the database schema:
php app/console doctrine:schema:update --force
Configure your application’s security
Edit your app/config/security.yml
file to add FOSOAuthServerBundle specific configuration:
# ... firewalls: oauth_token: # Everyone can access the access token URL. pattern: ^/oauth/v2/token security: false api: pattern: ^/api fos_oauth: true stateless: true anonymous: true # can be omitted as its default value # ... access_control: - { path: ^/api, role: IS_AUTHENTICATED_FULLY }
Create a client
Before you can generate tokens, you need to create a Client
using the ClientManager
. For this, create a new Symfony command:
<?php namespace ApiBundle\Command; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class OAuthAddClientCommand extends ContainerAwareCommand { protected function configure() { $this ->setName('oauth:add-client') ->setDescription("Ads a new client for OAuth") ; } protected function execute(InputInterface $input, OutputInterface $output) { $redirectUri = $this->getContainer()->getParameter('router.request_context.scheme') . "://" . $this->getContainer()->getParameter('router.request_context.host'); $clientManager = $this->getContainer()->get('fos_oauth_server.client_manager.default'); $client = $clientManager->createClient(); $client->setRedirectUris(array($redirectUri)); $client->setAllowedGrantTypes(array('refresh_token', 'password')); $clientManager->updateClient($client); } }
Now run the above command to generate your first OAuth client:
php app/console oauth:add-client
This client will be able to generate tokens and refresh tokens using the user’s username and password. You can find it’s data in the database client
table. The token
endpoint is at /oauth/v2/token
by default.
Document using NelmioApiDocBundle
If you use the NelmioApiDocBundle to document your API, you can add these OAuth methods too. Create a new YAML file in src/ApiBundle/Resources/apidoc/oauth.yml
:
grant_type_password: requirements: [] views: [] filters: [] parameters: grant_type: dataType: string required: true name: grant_type description: Grant Type (password) readonly: false client_id: dataType: string required: true name: client_id description: Client Id readonly: false client_secret: dataType: string required: true name: client_secret description: client Secret readonly: false username: dataType: string required: true name: username description: Username readonly: false password: dataType: string required: true name: password description: Password readonly: false input: null output: null link: null description: "Get OAuth token for user using username and password" section: "OAuth" documentation: null resource: null method: "POST" host: "" uri: "/oauth/v2/token" response: token: dataType: string required: true description: OAuth token readonly: true route: path: /oauth/v2/token defaults: _controller: FOS\UserBundle\Controller\SecurityController::checkAction requirements: [] options: compiler_class: Symfony\Component\Routing\RouteCompiler host: '' schemes: [] methods: [ 'POST' ] condition: '' https: false authentication: false authenticationRoles: [] cache: null deprecated: false statusCodes: [] resourceDescription: null responseMap: [] parsedResponseMap: [] tags: [] grant_type_refresh_token: requirements: [] views: [] filters: [] parameters: grant_type: dataType: string required: true name: grant_type description: Grant Type (refresh_token) readonly: false client_id: dataType: string required: true name: client_id description: Client Id readonly: false client_secret: dataType: string required: true name: client_secret description: client Secret readonly: false refresh_token: dataType: string required: true name: refresh_token description: Refresh token readonly: false input: null output: null link: null description: "Get new OAuth token using refresh token" section: "OAuth" documentation: null resource: null method: "POST" host: "" uri: "/oauth/v2/token" response: token: dataType: string required: true description: OAuth token readonly: true route: path: /oauth/v2/token defaults: _controller: FOS\UserBundle\Controller\SecurityController::checkAction requirements: [] options: compiler_class: Symfony\Component\Routing\RouteCompiler host: '' schemes: [] methods: [ 'POST' ] condition: '' https: false authentication: false authenticationRoles: [] cache: null deprecated: false statusCodes: [] resourceDescription: null responseMap: [] parsedResponseMap: [] tags: []
Add a new NelmioApiYmlProvider.php
file in src/ApiBundle/Service
folder:
<?php namespace ApiBundle\Service; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Nelmio\ApiDocBundle\Extractor\AnnotationsProviderInterface; use Symfony\Component\Finder\Finder; use Symfony\Component\Routing\Route; use Symfony\Component\Yaml\Yaml; /** * Generate annotations for vendor routes to be displayed in Nelmio ApiDoc. */ class NelmioApiYmlProvider implements AnnotationsProviderInterface { private $vendorFolder; public function __construct($vendorFolder) { $this->vendorFolder = $vendorFolder; } /** * {@inheritdoc} */ public function getAnnotations() { $annotations = []; $configDirectories = array($this->vendorFolder); $finder = new Finder(); $finder->files()->in($configDirectories); if (count($finder) == 0) { return $annotations; } foreach ($finder as $file_) { $data = Yaml::parse(file_get_contents($file_)); $vendors = array_keys($data); foreach ($vendors as $vendor) { $apiDoc = new ApiDoc($data[$vendor]); $route = new Route( $data[$vendor]['route']['path'], $data[$vendor]['route']['defaults'], $data[$vendor]['route']['requirements'], $data[$vendor]['route']['options'], $data[$vendor]['route']['host'], $data[$vendor]['route']['schemes'], $data[$vendor]['route']['methods'], $data[$vendor]['route']['condition'] ); $apiDoc->setRoute($route); $apiDoc->setResponse($data[$vendor]['response']); $annotations[] = $apiDoc; } } return $annotations; } }
Add a new service in src/ApiBundle/Resources/config/services.yml
file:
services: nelmio_api_doc.yml_provider.api_yml_provider: class: ApiBundle\Service\NelmioApiYmlProvider arguments: folder: %kernel.root_dir%/../src/ApiBundle/Resources/apidoc tags: - { name: nelmio_api_doc.extractor.annotations_provider }
You’ll find now two /oauth/v2/token
methods with different parameters listed in the api/doc
section of your project.
That’s all! You can now use the generated client to authenticate your users in your mobile app using OAuth.
How to use the FOSOAuthServerBundle
First you will need to get an access token by making a POST request to the /oauth/v2/token
endpoint with the following parameters:
grant_type=password client_id=[client's id from the database followed by '_' then the corresponding random id] client_secret=[client's secret] username=[user's username] password=[users's password]
You should get back something like this:
{ "access_token": "ZDgxZDlkOWI2N2IyZWU2ZjlhY2VlNWQxNzM0ZDhlOWY2ZTIwOTBkNGUzZDUyOGYxOTg1ZTRjZGExOTY2YjNmNw", "expires_in": 3600, "token_type": "bearer", "scope": null, "refresh_token": "MDQ3MGIwZTk5MDkwOGM5NjhkMzk5NTUyZDJjZmYwM2YzZWViZDFhZjk0NTIyZmNjNzkyMDM0YjM4ODQ2N2VhNg" }
Use the access token for authenticated requests by placing it in the request header:
Authorization: Bearer ZDgxZDlkOWI2N2IyZWU2ZjlhY2VlNWQxNzM0ZDhlOWY2ZTIwOTBkNGUzZDUyOGYxOTg1ZTRjZGExOTY2YjNmNw
When the access token expires, you can get a new one using the refresh_token
grant type at the same /oauth/v2/token
endpoint:
grant_type=refresh_token client_id=[client's id from the database followed by '_' then the corresponding random id] client_secret=[client's secret] refresh_token=[refresh token received earlier]
The response should be similar to:
{ "access_token": "MjE1NjRjNDc0ZmU4NmU3NjgzOTIyZDZlNDBiMTg5OGNhMTc0MjM5OWU3MjAxN2ZjNzAwOTk4NGQxMjE5ODVhZA", "expires_in": 3600, "token_type": "bearer", "scope": null, "refresh_token": "YzM2ZWNiMGQ5MDBmOGExNjhmNDI1YjExZTkyN2U0Mzk5ZmM4NzcwNDdhNjAzZDliMjY3YzE0ZTg5NDFlZjg3MQ" }
How to add TINYINT MySQL type to Doctrine in Symfony 2.8
Hello! A few days ago I needed to define an entity with a TINYINT type column to hold a rating value between 1 and 5. I was doing this in a Symfony 2.8 project and to my surprise, Doctrine was unable to handle this type of data out of the box (it uses it for booleans but it has no TINYINT standalone type).
So I searched the internet for a solution and I found some but nothing complete.
The solution (TINYINT vs Doctrine):
First you need to define the new Doctrine type class like this:
<?php namespace AppBundle\Types; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Platforms\AbstractPlatform; class TinyintType extends Type { const TINYINT = 'tinyint'; public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) { return 'TINYINT'; } public function convertToPHPValue($value, AbstractPlatform $platform) { return $value; } public function convertToDatabaseValue($value, AbstractPlatform $platform) { return $value; } public function getName() { return self::TINYINT; } }
Then, to register it, I used the boot function of the AppBundle:
<?php namespace AppBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; use Doctrine\DBAL\Types\Type; class AppBundle extends Bundle { public function boot() { parent::boot(); $em = $this->container->get('doctrine.orm.default_entity_manager'); // types registration if(!Type::hasType('tinyint')) { try { Type::addType('tinyint', 'AppBundle\Types\TinyintType'); $em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('TINYINT', 'tinyint'); } catch (\Exception $e) { } } } }
I had to use the try / catch block to avoid an error when running symfony console commands having no database defined:
PHP Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[42000] [1049] Unknown database 'rating'' in vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php:43
Now you can use the new type in your entities:
/** * @ORM\Column(name="rating", type="tinyint") * @Assert\NotBlank() * @Assert\GreaterThanOrEqual( * value = 1 * ) * @Assert\LessThanOrEqual( * value = 5 * ) */ protected $rating;
Learn how to setup Docker machine for Symfony development.
Hope this helps someone :)
Have a great day!
Setup Docker Machine for Symfony Development
If you need a development environment for Symfony, hosted by Docker, you can use the fazy/apache-symfony docker image to start with, then extend it to your need.
Setup Docker Machine for Symfony Development
In your Symfony project's folder add the Dockerfile
and docker-compose.yml
files.
In the first one, we added mysql
and composer
, then we used the docker-compose.yml
file to mount a local volume to the docker container with file sync.
This way you can code on your machine and test in the docker environment.
The Dockerfile
:
FROM fazy/apache-symfony ADD . /app RUN apt-get update \ && apt-get -yq install \ mysql-server \ php5-mysql \ && rm -rf /var/lib/apt/lists/* RUN curl -s https://getcomposer.org/installer | php RUN mv composer.phar /usr/local/bin/composer ENV COMPOSER_ALLOW_SUPERUSER 1 RUN service mysql start
The docker-compose.yml
file:
version: "2" services: site: build: . ports: - "8080:80" expose: - "80" volumes: - .:/app
To build the Docker container use:
docker-compose up --build
To start it when it's down just remove the --build parameter:
docker-compose up
Once is running, you can view it's ID by running:
docker ps
Now grab the first 3 characters from the id and use them in the next command to be able to log in the running container:
docker exec -t -i ID bash
Finally, to access the Symfony project in your browser, go to http://localhost:8080/app_dev.php and have fun!
Setup Testing and Fixtures in Symfony2: The Easy Way
Setting up testing and fixtures in Symfony2 is vital if you plan on starting Test Driven Development or you simply want to start covering your code with properly written tests that can access mock data.
1. Install PHPUnit and php 5.6
The first thing you need to do is to install PHPUnit on your machine:
$ wget https://phar.phpunit.de/phpunit.phar $ chmod +x phpunit.phar $ sudo mv phpunit.phar /usr/local/bin/phpunit $ phpunit --version
and then, if needed, also upgrade your PHP version to 5.6:
$ sudo apt-get install language-pack-en-base $ export LC_CTYPE="en_US.UTF-8" $ sudo add-apt-repository ppa:ondrej/php5-5.6 $ sudo apt-get update $ sudo apt-get install php5
and make sure everything's ok by running: phpunit -c app/
Please note that this is not a testing tutorial so if you'd like to learn more about how to actually test your Symfony2 app, then please read their documentation.
2. Setup and create a test database
In order to be able to configure testing and fixtures in a Symfony2 app a separate, independent database is needed so that your dev environment is not affected.
In the config_test.yml
file you simply need to add:
doctrine: dbal: host: 127.0.0.1 dbname: testdb user: [YOUR_MYSQL_USERNAME] password: [YOUR_MYSQL_PASSWORD]
then simply run php app/console doctrine:database:create --env=test
Related: Send Emails in Symfony2: The Right Way
3. Build the BaseTestSetup class
Once our test database is ready we can start building our BaseTestSetup
class, one which will serve as parent for all of our tests.
<?php namespace AppBundle\Tests; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; abstract class BaseTestSetup extends WebTestCase { protected $client; protected $container; protected $em; protected function setUp() { $this->client = static::createClient(); $this->container = $this->client->getContainer(); $this->em = static::$kernel->getContainer() ->get('doctrine') ->getManager(); } }
4. Install the LiipFunctionalTestBundle
Even though Symfony does have an out of the box solution to setup and test your app, the LiipFunctionalTestBundle provides base classes for functional tests to assist in setting up testing and fixtures and HTML5 validation.
After you install and configure the bundle, go back to your BaseTestSetup
class and make the necessary modifications:
<?php namespace AppBundle\Tests; use Doctrine\ORM\Tools\SchemaTool; use Liip\FunctionalTestBundle\Test\WebTestCase; abstract class BaseTestSetup extends WebTestCase { protected $client; protected $container; protected $em; protected function setUp() { $this->client = static::createClient(); $this->container = $this->client->getContainer(); $this->em = static::$kernel->getContainer() ->get('doctrine') ->getManager(); if (!isset($metadatas)) { $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); } $schemaTool = new SchemaTool($this->em); $schemaTool->dropDatabase(); if (!empty($metadatas)) { $schemaTool->createSchema($metadatas); } $this->postFixtureSetup(); $this->loadFixtures(array( 'AppBundle\DataFixtures\ORM\LoadUserData', )); } }
The new code above simply drops and creates the database each them tests run. Then it loads the fixtures that you'll need. The LoadUserData
class does not exist yet so we'll go right ahead and add it by following the handy tutorial found on Symfony's website.
5. Write the very first test
Now that have your fixtures ready, you can go ahead and write your first test. Create a new file in your AppBundle/Tests
folder called UserTest.php
, next to BaseTestSetup.php
:
<?php namespace AppBundle\Tests; use AppBundle\Tests\BaseTestSetup; class UserTest extends BaseTestSetup { public function testSuggestImprovementEmail() { // assuming that you named your user 'username' in your Fixtures $crawler = $this->client->request('GET', '/show/username'); $this->assertGreaterThan( 0, $crawler->filter('html:contains("Hello first user!")')->count() ); } }
And that's about it! You now have everything you need to properly test your awesome app.
How to monitor Symfony and Apache logs with M/Monit
So you have developed and deployed a fresh new project and you want to be alerted in case an error happens.
Here's how you do it using the M/Monit tool.
It can be installed on an Ubuntu machine using the following command:
sudo apt-get install monit
Now edit the /etc/monit/monitrc
configuration file and add the following:
set mailserver smtp.sendgrid.net port 587 username "USERNAME" password "PASSWORD" set alert YOUR_EMAIL@HOST.COM not on { instance, action } set httpd port 2812 and use address localhost # only accept connection from localhost allow localhost # allow localhost to connect to the server and allow admin:monit # require user 'admin' with password 'monit' check file error.log with path /var/log/apache2/error.log if match "error" then alert check file prod.log with path /var/www/sfproject/app/logs/prod.log if match "request.CRITICAL" then alert
First we need to tell M/Monit what mail server to use (we used Sendgrid but you can use whatever you want), then to specify the email address which will receive the alerts. We also have to allow connections from localhost so the status
command below can be executed.
The actual monitoring is set up in the last two blocks of code, one for the apache error log and the next for the Symfony production logs. Anytime the specified string will appear in the logs, an alert email will be sent to the address we defined above.
To enable the changes, reload configuration:
monit reload
You can now check the status with the command:
monit status
It's that simple!
Send Emails in Symfony2: The Right Way
No matter what your app is about, at one you point you will have to send emails in Symfony2. Even though this process might seem straightforward and easy to apply, it can actually get pretty tricky. Here's how you can send emails in Symfony2 by properly splitting your functionality and templates.
1. Mailer parameters
First off, you're going to need to add some email data into your parameters.yml
file that will tell Swift Mailer which SMTP server to use. At this point, it would be advised to create an account at an email sending service. For this short tutorial, we're going to use SendGrid.
mailer_transport: smtp mailer_host: smtp.sendgrid.net mailer_user: your_sendgrid_username mailer_password: 'your_sendgrid_password' mailer_port: 587 contact_email: contact@yourapp.com from_email: hello@yourapp.com from_name: YourApp
As you can see, we've also added three parameters that will help us to send emails in Symfony2: contact email, from email and from name.
2. Send emails in Symfony2 using a MailManager
Having a MailManager is helpful because the code needed to send emails will only reside in a single place. Thus, it will be way easier to maintain. Here's how you should build the MailManager class:
<?php namespace AppBundle\Lib; class MailManager { protected $mailer; protected $twig; public function __construct(\Swift_Mailer $mailer, \Twig_Environment $twig) { $this->mailer = $mailer; $this->twig = $twig; } /** * Send email * * @param string $template email template * @param mixed $parameters custom params for template * @param string $to to email address or array of email addresses * @param string $from from email address * @param string $fromName from name * * @return boolean send status */ public function sendEmail($template, $parameters, $to, $from, $fromName = null) { $template = $this->twig->loadTemplate('AppBundle:Mail:' . $template . '.html.twig'); $subject = $template->renderBlock('subject', $parameters); $bodyHtml = $template->renderBlock('body_html', $parameters); $bodyText = $template->renderBlock('body_text', $parameters); try { $message = \Swift_Message::newInstance() ->setSubject($subject) ->setFrom($from, $fromName) ->setTo($to) ->setBody($bodyHtml, 'text/html') ->addPart($bodyText, 'text/plain') ; $response = $this->mailer->send($message); } catch (\Exception $ex) { return $ex->getMessage(); } return $response; } }
In the sendEmail
function, you can easily define a $template
variable which will hold whatever template you need, depending on the type of email. You'll see the MailManager
in action in the 4th and last section of this short tutorial, where you'll use it to send a contact email.
Oh, and don't forget to register the service in services.yml
:
mail_manager: class: AppBundle\Lib\MailManager arguments: ["@mailer", "@twig"]
Related: Symfony2 Facebook and Google Login: The Easy Way
3. Define a simple template
In this section you'll be building the three main blocks any email should have (subject, html and text) and store them in a template which you'll extend as needed.
{# src/AppBundle/Mail/template.html.twig #} {% block subject %} default subject {% endblock %} {% block body_text %} default text {% endblock %} {% block body_html %} <p>default body</p> {% endblock %}
4. A sample email sending action
Now that you have everything set, you can go ahead and build your first mail sending action. As an example, here's how a contact action should look like. It will send emails to the email address defined in parameters.yml
.
public function contactAction(Request $request) { $form = $this->createForm(new ContactType()); $form->handleRequest($request); //this is where we define which template to use $template = 'contact'; if($form->isValid()){ //Get data from the submitted form $data = $form->getData(); $mail_params = array( 'firstName' => $data["firstName"], 'lastName' => $data["lastName"], 'message' => $data["message"], 'phoneNumber' => $data["phoneNumber"], 'email' => $data["email"] ); //grab the addresses defined in parameters.yml $to = $this->container->getParameter('contact_email'); $from = $this->container->getParameter('from_email'); $fromName = $this->container->getParameter('from_name'); //use the MailManager service to send emails $message = $this->container->get('mail_manager'); $message->sendEmail($template, $mail_params, $to, $from, $fromName); return $this->redirectToRoute('contact'); } return $this->render('AppBundle:StaticPages:contact.html.twig',array( 'form' => $form->createView() )); }
Since you'll be using the contact
email body, you will need to build it by extending the template defined at step #3:
{% extends "AppBundle:Mail:template.html.twig" %} {% block subject %} YourApp Contact Message {% endblock %} {% block body_text %} {% autoescape false %} Name: {{ firstName }} {{ lastName }} Message: {{ message }} Phone Number: {{ phoneNumber }} Email address: {{ email }} {% endautoescape %} {% endblock %} {% block body_html %} {% autoescape false %} <p>Name: {{ firstName }} {{ lastName }}</p> <p>Message: {{ message }}</p> <p>Phone Number: {{ phoneNumber }}</p> <p>Email address: {{ email }}</p> {% endautoescape %} {% endblock %}
And there you have it - an easy way to build and maintain your emails. If you know of any other easier system used to send emails in Symfony2, please let me know in the comment section below.
Symfony2 Facebook and Google Login: The Easy Way
Since pretty much any potential user of an app has a Facebook or a Google account, it's crucial to have a login or registration service using these social networks. Luckily, HWIOAuthBundle provides the necessary tools to accomplish this. So here's an easy and straightforward way to integrate HWIOAuth in our app alongside FOSUserBundle:
Symfony2 Facebook and Google Login: The Easy Way
1. Configure HWIOauthBundle
Assuming that you've already installed and configured FOSUserBundle, we now need to add HWIOAuthBundle in our application. As mentioned in their setup page, we simply have to:
composer require hwi/oauth-bundle
Then we should enable the bundle in the kernel:
// app/AppKernel.php public function registerBundles() { $bundles = array( // ... new HWI\Bundle\OAuthBundle\HWIOAuthBundle(), ); }
and then we can import the social routes:
# app/config/routing.yml hwi_oauth_redirect: resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml" prefix: /connect hwi_oauth_login: resource: "@HWIOAuthBundle/Resources/config/routing/login.xml" prefix: /login
Once we have these setup, we can start configuring the security details in security.yml
:
firewalls: main: pattern: ^/ oauth: failure_path: your_path login_path: your_path check_path: /connect_check provider: fos_userbundle resource_owners: facebook: "/login/check-facebook" google: "/login/check-google" oauth_user_provider: service: app.provider.oauth
And then we can finally add Facebook and Google into HWIOAuth's configuration in config.yml
:
hwi_oauth: # name of the firewall in which this bundle is active, this setting MUST be set firewall_name: main connect: account_connector: app.provider.oauth resource_owners: facebook: type: facebook client_id: %facebook_client_id% client_secret: %facebook_client_secret% scope: "email, public_profile" infos_url: "https://graph.facebook.com/me?fields=id,name,email,picture.type(large)" paths: email: email options: display: page google: type: google client_id: %google_client_id% client_secret: %google_client_secret% scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile" paths: email: email
2. Build our own user provider by extending HWIOAuth's FOSUBUserProvider
As you've probably noticed, we used app.provider.oauth
in both security.yml
and config.yml
. So we need to define that service in services.yml
:
app.provider.oauth: class: AppBundle\Security\Core\User\OAuthUserProvider arguments: [@fos_user.user_manager,{facebook: facebookID, google: googleID}]
And then build it in our AppBundle:
<?php namespace AppBundle\Security\Core\User; use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface; use HWI\Bundle\OAuthBundle\Security\Core\User\FOSUBUserProvider as BaseClass; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserChecker; use Symfony\Component\Security\Core\User\UserInterface; /** * Class OAuthUserProvider * @package AppBundle\Security\Core\User */ class OAuthUserProvider extends BaseClass { /** * {@inheritdoc} */ public function loadUserByOAuthUserResponse(UserResponseInterface $response) { $socialID = $response->getUsername(); $user = $this->userManager->findUserBy(array($this->getProperty($response)=>$socialID)); $email = $response->getEmail(); //check if the user already has the corresponding social account if (null === $user) { //check if the user has a normal account $user = $this->userManager->findUserByEmail($email); if (null === $user || !$user instanceof UserInterface) { //if the user does not have a normal account, set it up: $user = $this->userManager->createUser(); $user->setEmail($email); $user->setPlainPassword(md5(uniqid())); $user->setEnabled(true); } //then set its corresponding social id $service = $response->getResourceOwner()->getName(); switch ($service) { case 'google': $user->setGoogleID($socialID); break; case 'facebook': $user->setFacebookID($socialID); break; } $this->userManager->updateUser($user); } else { //and then login the user $checker = new UserChecker(); $checker->checkPreAuth($user); } return $user; } }
Related: What Nobody Tells You When You're a Junior Developer
3. Override the User Model
In our user provider defined above we modified two attributes that do not exist in FOSUser's User model: $facebookId
and $googleId
. So we need to override the User model and add them.
namespace AppBundle\Entity; use FOS\UserBundle\Model\User as BaseUser; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass="AppBundle\Entity\UserRepository") * @ORM\Table(name="fos_user") */ class User extends BaseUser { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @var string * * @ORM\Column(name="facebook_id", type="string", nullable=true) */ private $facebookID; /** * @var string * * @ORM\Column(name="google_id", type="string", nullable=true) */ private $googleID; // ... }
Don't forget to update your database and then you can move forward to the next step.
4. Add Facebook and Google to our login and register pages
Now we're all set to update our Twig files with HWIOauth's paths. Since we're using FOSUser, we've overridden their register.html.twig
and login.html.twig
files and then added the social links:
{# ... #} <a href="{{ path('hwi_oauth_service_redirect', {'service': 'facebook' }) }}"> <span>Facebook</span> </a> <a href="{{ path('hwi_oauth_service_redirect', {'service': 'google' }) }}"> <span>Google</span> </a> {# ... #}
And there you have it: a fully functional Facebook and Google login and signup system. Now we can go to Facebook Developers and Google Developers Console, create our apps and add our client_id
and client_secret
in config.yml
. Have fun!
If you have a different take on this process and can make it even easier, please let us know in the comment section below.
Getting Started with Building APIs in Symfony2
Hello all you Interwebs friends! While we're passing through the shallow mists of time, REST is becoming more and more of a universal standard when building web applications. That said, here's a very brief tutorial on how to get started with building APIs in Symfony2.
Spoiler alert: the bits of code written below use FosUserBundle + Doctrine.
1. Generate a New Bundle
It's nice to keep your code neat and tidy, so ideally, you should create a separate bundle that will only store your API code, we can generically name ApiBundle.
$ php app/console generate:bundle --bundle-name=ApiBundle --format=annotation
2. Versioning
This isn't by any means mandatory, but if you believe that your API endpoints will suffer major changes along the way, that cannot be predicted from the get go, it would be nice to version your code. This means that you would, initially, have a prefix in your endpoints, like: `/v1`/endpoint.json, and you'd increase that value each time a new version comes along. I'll describe how to actually create the first version (`v1`) of your API a little further down the line.
3. Install a Few Useful Bundles
- FosRestBundle - this bundle will make our REST implementation a lot easier.
$ composer require friendsofsymfony/rest-bundle
and then include FOSRestBundle in your `AppKernel.php` file:
$bundles = array( // ... new FOS\RestBundle\FOSRestBundle(), );
- JmsSerializerBundle - this will take care of the representation of our resources, converting objects into JSON.
composer require jms/serializer-bundle
and then include JMSSerializerBundle in your `AppKernel.php`:
$bundles = array( // ... new JMS\SerializerBundle\JMSSerializerBundle(), // ... );
4. Configurations
Configure the Response object to return JSON, as well as set a default format for our API calls. This can be achieved by adding the following code in `app/config/config.yml`:
fos_rest: format_listener: rules: - { path: ^/api/, priorities: [ html, json, xml ], fallback_format: ~, prefer_extension: true } routing_loader: default_format: json param_fetcher_listener: true view: view_response_listener: 'force' formats: xml: true json: true templating_formats: html: true
5. Routing
I prefer using annotations as far as routes go, so all we need to do in this scenario would be to modify our API Controllers registration in `/app/config/routing.yml`. This registration should have already been created when you ran the generate bundle command. Now we'll only need to add our version to that registration. As far as the actual routes of each endpoint go, we'll be manually defining them later on, in each action's annotation.
api: resource: "@ApiBundle/Controller/V1/" type: annotation prefix: /api/v1
At this point we're all set to start writing our first bit of code. First off, in our Controller namespace, we would want to create a new directory, called `V1`. This will hold all of our v1 API Controllers. Whenever we want to create a new version, we'll start from scratch by creating a `V2` namespace and so on.
After that's done let's create an action that will GET a user (assuming we've previously created a User entity and populated it with users). This would look something like:
<?php namespace ApiBundle\Controller\V1; use FOS\RestBundle\Controller\FOSRestController; use FOS\RestBundle\Controller\Annotations as Rest; class UserController extends FOSRestController { /** * @return array * @Rest\Get("/users/{id}") * @Rest\View */ public function getUserAction($id) { $em = $this->getDoctrine()->getManager(); $user = $em->getRepository('AppBundle:User')->find($id); return array('user' => $user); } }
If we want to GET a list of all users, that's pretty straightforward as well:
/** * GET Route annotation. * @return array * @Rest\Get("/users/get.{_format}") * @Rest\View */ public function getUsersAction() { $em = $this->getDoctrine()->getManager(); $users = $em->getRepository('AppBundle:User')->findAll(); return array('users' => $users); }
With that done, when running a GET request on `https://yourapp.com/api/v1/users/1.json`, you should get a `json` response with that specific user object.
What about a POST request? Glad you asked! There are actually quite a few options to do that. One would be to get the request data yourself, validate it and create the new resource yourself. Another (simpler) option would be to use Symfony Forms which would handle all this for us.
The scenario here would be for us to add a new user resource into our database.
If you're also using FosUserBundle to manage your users, you can just use a similar RegistrationFormType:
<?php namespace ApiBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class RegistrationFormType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('email', 'email') ->add('username') ->add('plainPassword', 'password') ; } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => 'AppBundle\Entity\User', 'csrf_protection' => false )); } public function getName() { return 'my_awesome_form'; } }
Next, we'll want to actually create our addUserAction():
/** * POST Route annotation. * @Rest\Post("/users/new.{_format}") * @Rest\View * @return array */ public function addUserAction(Request $request) { $userManager = $this->container->get('fos_user.user_manager'); $user = $userManager->createUser(); $form = $this->createForm(new \ApiBundle\Form\Type\RegistrationFormType(), $user); $form->handleRequest($request); if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($user); $em->flush(); return array('user' => $user); } return View::create($form, 400); }
To make a request, you'll need to send the data as raw JSON, to our `https://yourapp.com/api/v1/users/new.json` POST endpoint:
{ "my_awesome_form":{ "email":"andrei@ibw.com", "username":"sdsa", "plainPassword":"asd" } }
And that's all there is to it.
We haven't covered the cases where you'd want Delete or Update a user yet. Updating resources through the REST standards can be done using either PUT or PATCH. The difference between them is that PUT will completely replace your resource, while PATCH will only, well, patch it... meaning that it will partially update your resource with the input it got from the API request.
Let's get to it then. We'll use the same form as before, and we'll try to only change (we'll use the PATCH verb for that) the email address, username and password of our previously created user:
/** * PATCH Route annotation. * @Rest\Patch("/users/edit/{id}.{_format}") * @Rest\View * @return array */ public function editAction(Request $request, $id) { $userManager = $this->container->get('fos_user.user_manager'); $user = $userManager->findUserBy(array('id'=>$id)); $form = $this->createForm(new \ApiBundle\Form\Type\RegistrationFormType(), $user, array('method' => 'PATCH')); $form->handleRequest($request); if ($user) { if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($user); $em->flush(); return array('user' => $user); } else { return View::create($form, 400); } } else { throw $this->createNotFoundException('User not found!'); } }
The request body is the same as the one shown for the POST method, above. There are a few small differences in our edit action though. First off - we're telling our form to use the PATCH method. Second - we are handling the case where the user ID provided isn't found.
The Delete method is the easiest one yet. All we need to do is to find the user and remove it from our database. If no user is found, we'll throw a "user not found" error:
/** * DELETE Route annotation. * @Rest\Delete("/users/delete/{id}.{_format}") * @Rest\View(statusCode=204) * @return array */ public function deleteAction($id) { $em = $this->getDoctrine()->getManager(); $user = $em->getRepository('AppBundle:User')->find($id); $em->remove($user); $em->flush(); }
Related: Routing in Symfony2
Conclusions
Aaand we're done. We now have a fully working CRUD for our User Entity. Thanks for reading and please do share your opinions/suggestions in the comment section below.