Skip to main content

Learning about Symfony

I recently set up a simple Symfony site to learn more about it. Since Symfony is the foundation of Drupal 8, which I've worked with before, I figured learning Symfony would help me to understand Drupal 8 better. Also, Symfony is like a disassembled site of components, which is a useful resource for a web developer to know about. I can think of circumstances where you don't need a full CMS. Like maybe you just need a simple site for some data collection with an authentication component so people can log-in. Symfony is also used in other PHP frameworks, like PatternLab, and Magento. So there are many reasons to be familiar with Symfony.

My project is a simple client portal site where people can register, log-in, and fill in a biography about themselves which can be displayed in a public list on the front page if the user chooses to publish it. The following is a summary of the process of creating the site working with Symfony 5.2.

Here's the end result:
https://clientportal.ebenshapiro.com

*A quick disclaimer, this is not a complete tutorial yet. For now, I'm just recording some notes I took for easy reference.

 

Request/Response

Probably the most recognizable aspect of Symfony in Drupal 8 is the basic flow of each page request. When the user sends a request to the site, the URL determines what Route is taken to fulfill the request. The Route sends the request to a controller function in the form of a Request object. And then within the controller a Response object is crafted to send back to the user. Here's an example of a basic controller:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

// You can name the controller anything you want.
// It will be autoloaded regardless of the name.
// The file name has to be the same though,
// so this file would be named DefaultController.php

class DefaultController extends AbstractController

{

    /** This annotation is how Symfony knows how to connect
     *  the /test route url to this controller member function.
     * @Route("/test", name="edittest")
     */

    public function Test (Request $request) {
        // Returns a plain text 
        return new Response("helloooo");
    }

    // There can be many controller functions within a controller.
    // The controller function below sends the response to a twig template.     
    // Templates are stored in /templates folder.

    /**
     * @Route("/", name="home")
     */
    public function index(): Response
    {
        return $this->render('default/index.html.twig', [
            "data" => "helloooo"
        ]);
    }

}

The above code defines two controller member functions, so when a user visits site.com/test, their request will be run through the DefaultController->Test controller member function, which will return a plain text response. The second controller function gets called when the user visits the front page. In that example, twig code is returned as the Response. Annotations are used to define the Routes. Annotations are a feature that Symfony uses to inject PHP objects with more functionality, like in this case, hooking up a controller function to a Route, or adding third-party functionality to an Entity, an example of which we'll see when setting up the VichUploader for uploading images. Annotations are basically PHP comments that Symfony will parse before executing the code and will automatically attach the functionality specified in the comments to the object below where the comment appears. You can also specify routes in /config/routes.yaml, this is good when you have a route that doesn't need a controller, an example of which we'll see when setting up the logout page for the Security Package. 

 

Configuration

The database configuration is in the /.env file as a connection string that looks like this:
DATABASE_URL="mysql://user:password@127.0.0.1:3306/projectname"

You can also set whether the environment is in prod or dev mode:
APP_ENV=dev

You can set up a local .env file by setting up a .env-local file with the local database connection string. And .env-local is set to be ignored by .gitignore.

 

Debugging

When an environment is in dev mode, debugging information is displayed in a fixed footer at the bottom of the page. And data can be dumped to the footer by using:

dump(obj)

within your code. You can also dump data within the twig template by using:

{{ dump(obj) }}

which will print out the data in the part of the page that corresponds to where it is printed out in the template.

 

Installing Packages

Packages are installed with the following command:

composer require package-name

As late as Symfony 3, when you installed a Symfony package you also had to include the file path to that package in /config/bundles.php. Now, starting in Symfony 4, the concept of "recipes" has been introduced, so that as soon as a package is installed, a recipe is run to do any setup that is required. These are the packages used for this project:

 

The Vich Uploader is used to store images that a user uploads and the location of the image is saved to a field in a database table.

composer require vich/uploader-bundle

 

Install fixtures for automating the uploading of dummy data to the database:

composer require --dev orm-fixtures

The dev flag indicates that the package should be added to the require-dev object in the composer.json file since this is not a production feature.

 

Webpack Encore is a package for compiling the front-end static content, like CSS and javascript.

composer require symfony/webpack-encore-bundle

It works with node, so this command needs to be run to install the node packages:

yarn install

 

Symfony Command-line Tool

One of the neat things about Symfony is that it comes with a command-line tool that comes with a server for running your project locally, and it can also be used for creating common Symfony objects, similar to the Drupal Console tool for Drupal 8. It can be installed with the setup installer:

https://symfony.com/download

 

Command-line Code Generation

To create a new Symfony project, run this command:

symfony new projectName --full

This will install all the basic components you need for a website. For example, it will install the Doctrine bundle for connecting to the database, the Security package for handling the user authentication, the Twig templating engine, among other things.

If your database connection string is set in your .env file, then you can run this command to create the database:

php bin/console doctrine:database:create

Each table in the database corresponds to an Entity Object. Drupal 8 also has an Entity object, but as far as I can tell from reviewing the code, it is not built off of Symfony's Entity object. Create the User entity with this command:

php bin/console make:user

Generate the code for created the database tables:

php bin/console make:migration

This will create a file in the /migrations folder. With these migration files, the database can be automatically built up repeatedly. Running this command will run all the scripts in the /migrations folder and build the database:

php bin/console doctrine:migrations:migrate

Create the registration form:

php bin/console make:form

Then in the interactive process connect the form to the User Entity. This will set up the form so it can be used to create new users, which is perfect for a registration form.

We'll also need a log-in form. Symfony provides sample code we can use here:
https://symfony.com/doc/current/security/form_login_setup.html

 

Security Package

The Security bundle is automatically installed in the web install of Symfony. Hook up the User entity with the Security Package. The Security Package is a bundle of software that handles all the user authentication logic for us. This is probably the most common requirement for an online site, so it's extremely helpful to have an open-source community-maintained package that you can install on your site instead of writing your own solution. All the setup is done in this configuration file:

/config/packages/security.yaml

You should see yaml code in there from when the package was installed, under this property path of security->firewalls->main, include this configuration:

            form_login:
                # Configure the Security Package to authenticate
                # the user on the login page
                login_path: login
                check_path: login
                
                # Configure the email and password fields of the form
                # on the login page to be the parameters that the 
                # Security Package expects
                username_parameter: 'email'
                password_parameter: 'password'

                # Turn on csrf       
                csrf_token_generator: security.csrf.token_manager
                
                # By default, after logging in, the login page sends
                # you to the page you were on before logging in, but the
                # configuration below makes it so the user is always sent
                # to the viewProfile path.
                always_use_default_target_path: true
                default_target_path: viewProfile


            logout:
                # When the user goes to the logout page, they'll be logged out
                # and the redirected to the / page.
                path: /logout
                target: /

 

VichUploader

VichUploader is the package that Symfony recommends for uploading images. For the Entity that is associated with the image add imageName, imageFile, imageSize, and updatedAt fields. imageFile won't be migrated to the database, it's just a dummy field for the VichUploader to hook into through annotations. In the database imageName should be of string type, imageSize should be of int type, and updatedAt should be of datetime type. This can be setup with the Symfony command-line tool. And to same the image, within your controller run this code which grabs the uploaded file from the form object.

$yourEntity = new YourEntity();
$form = $this->createForm(YourEntityUploaderType::class, $yourEntity);

// If there's form data in the request, then check if it's valid.
$form->handleRequest($request);
$yourEntity->setImageFile($formData->getImageFile());

 

Services

Services are functionality that you may want to call in multiple places. Services are used in Drupal 8 too. These are classes that Symfony will instantiate for you as a singleton object and automatically load it for you in the places where you configure it to load, like for use within a Controller. The Doctrine object for querying the database is a service. 

 

Fixtures

Fixtures allow you to quickly upload dummy content for the sake of previewing it.  They are defined in /src/DataFixtures. And this command runs the fixtures:

php bin/console doctrine:fixtures:load

 

Apache .htaccess Configuration

Symfony intends for the /public folder to be the webroot. My hosting service doesn't allow me to set the webroot of a subdomain, so the docroot and the webroot are the same. To get around this, I installed an .htaccess file in the subdomain's docroot to send all requests to the public folder:

RewriteEngine on 
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /public/$1 [QSA,L]

And then within the public folder is the .htaccess file installed by this package:

composer require symfony/apache-pack

 

Conclusion

All the information here can be used to create a simple site that needs to save user data. I think whether to use Symfony depends on the complexity of the site you're working on. Like if a person needed a blog, then I would recommend they use something like Drupal or Wordpress rather than build the whole thing in Symfony. But if you need to setup a quick site to have authentication that can be used with another platform then Symfony becomes like a developer jack-knife for setting up that functionality. Situations like this can come up when a company is sealed into using a particular platform, and all of a sudden a new requirement comes up that that platform can't support entirely, but it can be integrated with another web service to fulfill it. If that web service doesn't exist yet then that would be a perfect use for Symfony.

For example, Monday.com is a new job management platform. But in order to submit jobs, you need a user account, and Monday.com has user account limits. What if you want to allow hundreds of people to submit jobs? To get around Monday's account limitation, you could create a Symfony site with user management, and a form that submits jobs to Monday.com through the Monday API once they are logged in. And since the user's info is saved in the Symfony database, the client ID can be sent along automatically with each API request. 

 

Further Reading:

Drupal 8 core and Symfony components