Skip to content

Branching the Code

There are many ways to organize the workflow of code changes in a project. But working directly on the Git master branch and deploying directly to production without testing is probably not the best one.

Testing is not just about unit or functional tests, it is also about checking the application behavior with production data. If you or your stakeholders can browse the application exactly as it will be deployed to end users, this becomes a huge advantage and allows you to deploy with confidence. It is especially powerful when non-technical people can validate new features.

We will continue doing all the work in the Git master branch in the next steps for simplicity sake and to avoid repeating ourselves, but let's see how this could work better.

Adopting a Git Workflow

One possible workflow is to create one branch per new feature or bug fix. It is simple and efficient.

Creating Branches

The workflow starts with the creation of a Git branch:

1
$ git branch -D sessions-in-db || true
1
$ git checkout -b sessions-in-db

This command creates a sessions-in-db branch from the master branch. It "forks" the code and the infrastructure configuration.

Storing Sessions in the Database

As you might have guessed from the branch name, we want to switch session storage from the filesystem to a database store (our PostgreSQL database here).

The needed steps to make it a reality are typical:

  1. Create a Git branch;
  2. Update the Symfony configuration if needed;
  3. Write and/or update some code if needed;
  4. Update the PHP configuration if needed (like adding the PostgreSQL PHP extension);
  5. Update the infrastructure on Docker and Platform.sh if needed (add the PostgreSQL service);
  6. Test locally;
  7. Test remotely;
  8. Merge the branch to master;
  9. Deploy to production;
  10. Delete the branch.

To store sessions in the database, change the session.handler_id configuration to point to the database DSN:

1
2
3
4
5
6
7
8
9
10
11
--- a/config/packages/framework.yaml
+++ b/config/packages/framework.yaml
@@ -7,7 +7,7 @@ framework:
     # Enables session support. Note that the session will ONLY be started if you read or write from it.
     # Remove or comment this section to explicitly disable session support.
     session:
-        handler_id: null
+        handler_id: '%env(DATABASE_URL)%'
         cookie_secure: auto
         cookie_samesite: lax
         storage_factory_id: session.storage.factory.native

To store sessions in the database, we need to create the sessions table. Do so with a Doctrine migration:

1
$ symfony console make:migration

Edit the file to add the table creation in the up() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
--- a/migrations/Version00000000000000.php
+++ b/migrations/Version00000000000000.php
@@ -21,6 +21,14 @@ final class Version00000000000000 extends AbstractMigration
     {
         // this up() migration is auto-generated, please modify it to your needs

+        $this->addSql('
+            CREATE TABLE sessions (
+                sess_id VARCHAR(128) NOT NULL PRIMARY KEY,
+                sess_data BYTEA NOT NULL,
+                sess_lifetime INTEGER NOT NULL,
+                sess_time INTEGER NOT NULL
+            )
+        ');
     }

     public function down(Schema $schema): void

Migrate the database:

1
$ symfony console doctrine:migrations:migrate

Test locally by browsing the website. As there are no visual changes and because we are not using sessions yet, everything should still work as before.

Note

We don't need steps 3 to 5 here as we are re-using the database as the session storage, but the chapter about using Redis shows how straightforward it is to add, test, and deploy a new service in both Docker and Platform.sh.

As the new table is not "managed" by Doctrine, we must configure Doctrine to not remove it in the next database migration:

1
2
3
4
5
6
7
8
9
10
11
--- a/config/packages/doctrine.yaml
+++ b/config/packages/doctrine.yaml
@@ -5,6 +5,8 @@ doctrine:
         # IMPORTANT: You MUST configure your server version,
         # either here or in the DATABASE_URL env var (see .env file)
         #server_version: '13'
+
+        schema_filter: ~^(?!session)~
     orm:
         auto_generate_proxy_classes: true
         naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware

Commit your changes to the new branch:

1
2
$ git add .
$ git commit -m'Configure database sessions'

Deploying a Branch

Before deploying to production, we should test the branch on the same infrastructure as the production one. We should also validate that everything works fine for the Symfony prod environment (the local website used the Symfony dev environment).

Now, let's create a Platform.sh environment based on the Git branch:

1
$ symfony cloud:env:delete sessions-in-db
1
$ symfony cloud:deploy

This command creates a new environment as follows:

  • The branch inherits the code and infrastructure from the current Git branch (sessions-in-db);
  • The data come from the master (aka production) environment by taking a consistent snapshot of all service data, including files (user uploaded files for instance) and databases;
  • A new dedicated cluster is created to deploy the code, the data, and the infrastructure.

As the deployment follows the same steps as deploying to production, database migrations will also be executed. This is a great way to validate that the migrations work with production data.

The non-master environments are very similar to the master one except for some small differences: for instance, emails are not sent by default.

Once the deployment is finished, open the new branch in a browser:

1
$ symfony cloud:url -1

Note that all Platform.sh commands work on the current Git branch. This command opens the deployed URL for the sessions-in-db branch; the URL will look like https://sessions-in-db-xxx.eu-5.platformsh.site/.

Test the website on this new environment, you should see all the data that you created in the master environment.

If you add more conferences on the master environment, they won't show up in the sessions-in-db environment and vice-versa. The environments are independent and isolated.

If the code evolves on master, you can always rebase the Git branch and deploy the updated version, resolving the conflicts for both the code and the infrastructure.

You can even synchronize the data from master back to the sessions-in-db environment:

1
$ symfony cloud:env:sync

Debugging Production Deployments before Deploying

By default, all Platform.sh environments use the same settings as the master/prod environment (aka the Symfony prod environment). This allows you to test the application in real-life conditions. It gives you the feeling of developing and testing directly on production servers, but without the risks associated with it. This reminds me of the good old days when we were deploying via FTP.

In case of a problem, you might want to switch to the dev Symfony environment:

1
$ symfony cloud:env:debug

When done, move back to production settings:

1
$ symfony cloud:env:debug --off

Warning

Never enable the dev environment and never enable the Symfony Profiler on the master branch; it would make your application really slow and open a lot of serious security vulnerabilities.

Testing Production Deployments before Deploying

Having access to the upcoming version of the website with production data opens up a lot of opportunities: from visual regression testing to performance testing. Blackfire is the perfect tool for the job.

Refer to the step about Performance to learn more about how you can use Blackfire to test your code before deploying.

Merging to Production

When you are satisfied with the branch changes, merge the code and the infrastructure back to the Git master branch:

1
2
$ git checkout master
$ git merge sessions-in-db

And deploy:

1
$ symfony cloud:deploy

When deploying, only the code and infrastructure changes are pushed to Platform.sh; the data are not affected in any way.

Cleaning up

Finally, clean up by removing the Git branch and the Platform.sh environment:

1
2
$ git branch -d sessions-in-db
$ symfony cloud:env:delete -e sessions-in-db
This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.
TOC
    Version