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:
- Create a Git branch;
- Update the Symfony configuration if needed;
- Write and/or update some code if needed;
- Update the PHP configuration if needed (like adding the PostgreSQL PHP extension);
- Update the infrastructure on Docker and Platform.sh if needed (add the PostgreSQL service);
- Test locally;
- Test remotely;
- Merge the branch to master;
- Deploy to production;
- 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
Going Further