April 14, 2020 | Last updated: April 14, 2020
Share:

After reading this guide you will be able to:
- Install Symfony
- Create a new Symfony project
- Add a new functionality
- Create form, controller, and view
- Prepare a Continuous Integration pipeline
- Prepare a continuous delivery pipeline
- Deploy a working Symfony application to AWS Elastic Beanstalk
Actions used in this guide:
Creating Symfony project
Let’s begin with installing Symfony:
wget https://get.symfony.com/cli/installer -O - | bash
$
Next, create a new project and name it symfony-first-steps:
symfony new symfony-first-steps
$
Go to the project's folder and set up a dev server to check if everything went fine:
cd symfony-first-steps
symfony serve
$$
The project should launch at the default address 127.0.1:8000
Symfony welcome screen
Business logic
The project is ready, so let's have a look at the specs of the application to write: The application should accept any text and return the number of occurrence for each word in the received paragraph.
Gotcha. Grab your coffee and onwards we go!
PHPUnit installation
Begin with installing PHPUnit, an obvious part of test-driven development:
composer require --dev phpunit/phpunit
$
Making sure that it works should be obvious as well:
bin/phpunit
$
Writing tests
After some installation processing, a warning will appear that no tests were detected. Let’s write a test then:
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Service;
use App\Service\WordCounter;
use PHPUnit\Framework\TestCase;
final class WordCounterTest extends TestCase
{
/**
* @dataProvider textDataProvider
*/
public function testWordCounter(string $text, array $result): void
{
self::assertEquals($result, (new WordCounter())->count($text));
}
public function textDataProvider(): \Generator
{
yield 'basic text' => ['Lorem ipsum dolor sit amet, ipsum dolor sit.', [
'Lorem' => 1,
'ipsum' => 2,
'dolor' => 2,
'sit' => 2,
'amet' => 1,
]];
yield 'leetspeak text' => ['l0r3m 1p5um d0l0r 517 4m37, 1p5um d0l0r 517.', [
'l0r3m' => 1,
'1p5um' => 2,
'd0l0r' => 2,
'517' => 2,
'4m37' => 1,
]];
yield 'ignore spaces' => ['Some text with tabs and spaces', [
'Some' => 1,
'text' => 1,
'with' => 1,
'tabs' => 1,
'and' => 1,
'spaces' => 1,
]];
}
}
For the moment, one you run bin/phpunit
the tests will fail and color the console red. What you should do is add the implementation of the new service that counts words:
<?php
declare(strict_types=1);
namespace App\Service;
final class WordCounter
{
/**
* @return array<string,int>
*/
public function count(string $text): array
{
$words = explode(' ', preg_replace('/[^A-Za-z0-9?![:space:]]/', '', $text));
return array_reduce($words, function (array $counts, string $word): array {
if (trim($word) !== '') {
$counts[$word] = isset($counts[$word]) ? ++$counts[$word] : 1;
}
return $counts;
}, []);
}
}
Run bin/phpunit
again. Now, you should get the following:
Form development
With the tests working, we can now create a form that will receive the text, transfer it to our service after an initial validation, and return it to the template to display it to the user. All of this will be handled by the controller working on the provided URL (in this case, just /
). In other words, the controller will be the entry point for our application. So let's start with creating a controller.
Controller
By default, each URL is configured in the config/routes.yaml
file. For the needs of this guide, however, we will use annotations, which are a little more convenient. For that we'll need an additional package:
composer require annotations
$
Next, create a new file src/Controller/HomeController.php
with the following content:
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
final class HomeController
{
/**
* @Route("/")
*/
public function index(): Response
{
return new Response('Word Counter');
}
}
Once you refresh your browser (http://127.0.0.1:8000/), you'll see:
Word Counter
The first business value has been delivered – now we can move to the view.
View
The text returned from the controller was pretty simple. In reality, however, our intention will be to render a website in HTML. To avoid mixing HTML with PHP, we'll use a template engine. The default template engine for Symfony is Twig.
In the console, enter:
composer require twig
$
Next, create a new view in the templates
folder (it was created automatically by Twig). Add a new file templates/home.html.twig
with the following content:
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<div class="col">
<h1>Word Counter</h1>
<p>Count unique words in given text.</p>
</div>
</div>
</div>
{% endblock %}
Here we are using ready Bootstrap components. We shall add Bootstrap later while creating the form.
Right now we need to slightly modify our controller. To make the whole process easier, we'll inherit the AbstractController
class and use the render
method to generate our view. After the changes, the whole piece will look as follows:
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
final class HomeController extends AbstractController
{
/**
* @Route("/")
*/
public function index(): Response
{
return $this->render('home.html.twig');
}
}
The website will look like this:
Word Counter with subtitle
Form
We will now add a form that will allow you to receive user data, validate it, and pass it to our word counting service.
The recommended workflow when working with Symfony forms is the following:
- Build the form in a Symfony controller or using a dedicated form class;
- Render the form in a template so the user can edit and submit it;
- Process the form to validate the submitted data, transform it into PHP data and do something with it (e.g. persist it in a database).
Let's begin with fetching the package with forms:
composer require symfony/form
$
Symfony recommends to put as little logic as possible in the controllers. That's why it's better to move complex forms to dedicated classes instead of defining them in controller actions. Besides, forms defined in classes can be reused in multiple actions and services.
So in the file src/Form/Type/CountWordType.php
we should create the following form:
<?php
declare(strict_types=1);
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
final class CountWordType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('text', TextareaType::class, ['label' => 'Text to count'])
->add('submit', SubmitType::class, ['label' => 'Count words'])
;
}
}
Now we can add Bootstrap so that our form renders nicely and we can build our application brick by brick as in the good old days.
In the templates/home.html.twig
file, add the styles and JavaScript required by Bootstrap:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
{% endblock %}
</body>
</html>
Now, it’s time to insert the newly created form CountWordType
into our template with the main site. First we need to create it in the controller. The index action will look as follows:
public function index(): Response
{
$form = $this->createForm(CountWordType::class);
return $this->render('home.html.twig', [
'form' => $form->createView(),
]);
}
To insert the form into the template, we just need to add {{ form(form) }}
right below the first paragraph:
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<div class="col">
<h1>Word Counter</h1>
<p>Count unique words in given text.</p>
{{ form(form) }}
</div>
</div>
</div>
{% endblock %}
Now the website should look like this:
Word Counter with subtitle and form
The next step is adding form support to our controller. By filling out the sample form with text and clicking the Count words button, a new request is sent to our application. By using the Symfony form, we can handle it with one simple handleRequest
method:
public function index(Request $request): Response
{
$form = $this->createForm(CountWordType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
return $this->render('home.html.twig', [
'results' => $this->wordCounter->count($form->get('text')->getData()),
'form' => $form->createView(),
]);
}
return $this->render('home.html.twig', [
'form' => $form->createView(),
]);
}
As you can see, if the form was submitted and is correct, i.e. $form->isSubmitted() && $form->isValid()
, the service result is returned to the view:
'results' => $this->wordCounter->count($form->get('text')->getData())
We now only need to display the results by using the previously created template:
{% block body %}
<div class="container">
<div class="row">
<div class="col">
<h1>Word Counter</h1>
<p>Count unique words in given text.</p>
{% if results is defined %}
<h2>Results:</h2>
<ul style="column-count: 4;">
{% for word, count in results|sort|reverse %}
<li>{{ word }}: {{ count }}</li>
{% endfor %}
</ul>
{% endif %}
{{ form(form) }}
</div>
</div>
</div>
{% endblock %}
Continuous Integration
In this section we shall prepare a pipeline that will let you implement Continuous Integration into your process.
In order to merge the results, a series of factors must be fulfilled:
- The application is built correctly
- The application works correctly (tests)
- The application is prepared for deployment
All these things can be guaranteed by a properly configured pipeline.
Composer script
Before we configure the pipeline, let's prepare some Composer scripts. In the “scripts”
section of the composer.json
file add the following:
"phpunit": [
"bin/phpunit --colors=always"
],
"tests": [
"@phpunit"
]
This way, we are leaving some room for additional tools (e.g. PHPStan or PHP CS Fixer) without altering the testing script:
composer tests
$
Testing pipeline
At this stage, your code is ready to be committed and pushed to the repository. Create a new project in Buddy and attach the repo with our Symfony website.
Adding a new Symfony project to Buddy
Click Add a new pipeline, enter the name (e.g. "test") and set the trigger mode to On push. Select the Branch by wildcard option from the dropdown and enter refs/*
next to it. In this way, your pipeline will be triggered on every newly pushed change:
Pipeline configuration
With the pipeline defined, we can add the PHP action:
PHP in the action roster
Add the following to the commands box:
composer validate
composer install
composer tests
$$$
PHP action configuration
The pipeline prepared in this way will test our code against every push to the repository, just in line with the principle of Continuous Integration. Time to release our application to production! 👹
Continuous Delivery
Deployment pipeline
Let's add another pipeline focused on deployment. Set the name to “deploy” and select Master as the source branch. Make sure to set the trigger mode to manual to save yourself from unexpected trouble:
Deployment pipeline configuration
Now, look up the Elastic Beanstalk action in the Amazon action roster:
AWS action roster
You will be asked to define your integration. You can do it with a key, by making Buddy a trusted 3rd party, or by assuming a role:
AWS integration config
Select your AWS region and choose Create an application on the applications list.
Creating new application from the AWS EB action
In the AWS console, create a new EBS application and name it “word counter”:
Creating a new EBS app in the the AWS console
Go back to Buddy and refresh the list of applications. Select "word counter" as the deployment destination.
Selecting deployment destination in the EB action
A cool feature of Buddy is that it suggests actions connected with our workflow. In other words, once we add deployment to Elastic Beanstalk, Buddy will propose adding a monitoring action for the EB service:
Pipeline action suggestion
Once again, we need to choose the region, application, and environment.
The deployment pipeline now looks like this:
AWS EB deployment pipeline
Click the Run button to execute the pipeline. Buddy will fetch the latest version of the application, deploy it to Elastic Beanstalk, and run post-deployment health checks:
Post-deployment execution screen
The application is now available from the AWS board:
AWS board
EBS environment
After the first deployment, our app will not be working properly yet: we need to configure the EBS environment by indicating the main location for the web server. We can do it in the AWS console by modifying the Software section of the application's environment settings. In the Document root field, enter /public
as the value:
EBS configuration
Now we need to wait a bit until AWS uploads the config to the server and we can enjoy a fully working application! 🙌
A fully deployed Word Counter application

Towards Continuous Deployment
The last step is combining both pipelines into one CI/CD ecosystem. For that we'll use an extra action that will trigger the deployment from the testing pipeline. Go to the pipeline with the PHP and add the action Run another pipeline at the end:
Run next pipeline action
As you remember, the tests are executed for all branches in the project. However, since the deployment is restricted to the master branch only, we don't need to trigger it for pushes made to other branches. That's why we'll apply a trigger condition that will run the action only if the tests ran successfully on the master branch:
- Go to the Condition tab and select the ENV VAR from the dropdown list
- Set the variable to
$BUDDY_EXECUTION_BRANCH
and the value tomaster
Trigger condition configuration
In this way, we were able to build a fully automatic deployment pipeline. Every time your code will be pushed to the master branch (e.g. as a result of accepting a merge request) it will also be tested. If all the tests pass correctly, another pipeline will be launched that will deploy your application to production. As you can see, from the moment you push your code, everything will happen automatically. Employing this kind of delivery process is called Continuous Deployment.
Bonus: Conditional notifications
One more cool feature of Buddy are conditional notifications. For example, you can add a messaging service like Slack or Discord that will send a message to your QA team in case the tests have failed, or inform your DevOps team that something went wrong during the deployment. Such approach protects you from being flooded with irrelevant information and allows your team to quickly take action if required.
To send a notification whenever a pipeline fails, add it to Actions run on failure right below the primary actions:
Pipeline with conditional notification
Summary
Thank you for reading the guide, I hope everything worked as expected! 😎 Feel free to leave your questions and suggestions in the comments, or contact us via the live-chat – the support team is always super responsive when it comes to solving delivery problems or helping debug a deployment issue. Below you'll find some more resources that will help you optimize your PHP workflow (and learn a bit more about Buddy).
Cheers!
Arek
Related read
- Buddy Actions explained – all actions of Buddy arranged in categories and explained in detail
- 5 ways to deploy PHP applications – from git pull to Docker, choose the best deployment method for your workflow
- 11 examples of CI/CD pipelines – the most popular use-cases of day-to-day development automation
- How to use MySQL in PHP builds – learn how to properly attach the MySQL service to build containers

Arek Kondas
Software Architect @ Buddy
With Buddy even the most complicated CI/CD workflows take minutes to create
Sign up for Buddy CI/CD
Start a free trial