Docker for Symfony 4

This blog post is an introduction to devs who want to start using Docker with Symfony4. It will guide you through creating a Symfony 4 project running on Docker.

Before we start, you’ll need to install Docker in your machine. You can download it from the official website.

Once Docker is installed, I strongly recommend playing with the getting started guide. Here the guide for macs and here the guide for windows. However, if you’re lazy like me, just use this command to make sure it’s installed.

docker --version

Once all of that is out of the way, we can start with the Symfony 4 project and the Docker environment that will run it.

For the sake of the example, I’ve created a local environment running PHP5 to make things a bit trickier, since Symfony4 runs on PHP7.

Let’s try to create an empty SF4 skeleton on my local machine.

composer create-project symfony/skeleton symfony

Right off the bat I get the following error

Could not find package symfony/skeleton with stability stable in a version installable using your PHP version 5.6.29.

So as you can see, we’ve created an environment running PHP5 and we cannot create a SF4 project, which runs on PHP7. Docker should help us solve this problem 🙂

We have two cases I’m going to tap on.

Case 1: I’m creating a SF4 project from scratch and I want to set up a development environment with Docker.

Step1: Clone the docker-symfony4 repository which has the docker configuration files.

git clone

Step2: Create the Symfony project skeleton.

First off, let’s start docker containters with the project we just downloaded.

#cd to the location where you cloned the project
cd ~/Development/docker-symfony4
#start the containers
docker-compose up -d 

This command starts the containers. The parameter -d makes them run in the background. If you omit the -d you’ll see the log.

Docker should start building (if it’s the first time for these images) and running with the containers in the background.

We will need to create the symfony project inside the php image bash, since it’s the only place we have PHP7. Remember we still have PHP5 in our machine, so first thing is to log into the bash for the php7 image.

docker-compose exec php-fpm bash

Once in there, we’re in a PHP7 image, so we should be able to create the skeleton for the symfony project.

#inside php-fpm bash
composer create-project symfony/skeleton symfony

Step3: Move the contents of the skeleton into the root of the application.

Unless you want to change the config of the working dir inside the docker-composer.yml, we need the symfony project to be in the root folder. Moreover, we can not clone the contents into the root folder directly like so (composer create-project symfony/skeleton .) because the installer deletes the contents of the folder you’re cloning into. Since it’s too risky, this option is not allowed. More info here.

Long story short, I’ve found this is the cleaner way to do it.

#inside php-fpm bash
mv /application/symfony/* /application
mv /application/symfony/.* /application

Now we can delete the empty folder we used for creating the skeleton

#inside php-fpm bash
rm -Rf /application/symfony

Step4: Require the components.

We can require whatever components we need.

#inside php-fpm bash
cd /application

composer require annotations
composer require --dev profiler
composer require twig
composer require orm
composer require form
composer require form validator
composer require maker-bundle

These are just a few, feel free to add the ones you want.

Step5: Creating some sample code in Symfony 4 project.

Now lets create a controller to test a sample route to make sure everything works.

#inside php-fpm bash
cd /application

bin/console make:controller Pizza

Now, open a new chrome tab and type the following URL. The port is the one we set up in the docker-compose.yml – If you check the dictionary of the ngix config, you can see that port 8000 maps the 80, which is the usual webserver port.


We should now see our new controller action rendering a response.

Step6: Sync de database.

Finally, to sync the database, you need to update the .env file with the variables we set on the mysql image.

.env file that has been generated when requiring the orm package in Symfony4.


So if you check the docker-compose.yml file, you’ll see the credentials under the mysql configuration. So change the file to


Finally, let’s restart the containers

docker-compose down
docker-compose up -d
docker-compose exec php-fpm bash

Now inside the bash, you should be able to

#inside php-fpm bash
bin/console doc:sch:crea

You can connect an external database client such as Sequel Pro or MysqlWorkbench.

For the credentials remember to put again the ones we set in the mysql image.

Username: dbuser
Password: dbpw
Port: 8002

Also as a reminder, every time you composer down or kill the mysqld image, your schema will disapear! This not only means you’ll have to recreate again the following time, it also means the data will be lost. So make sure to dump the data if you need it later, or create some demo data/fixtures so you don’t have to add data manually.

Case 2: I already have a SF4 project

Maybe you cloned from elsewhere, or maybe you created it in the past with PHP7 in your local machine.

Step1: Clone the docker-symfony4 repository which has the docker configuration files.

git clone

Step2: Move the files from the docker-symfony4 project folder you just cloned into your Symfony project root.

Move the docker-compose.yml and the folder named phpdocker containing nginx and php-fpm config for it to the root of your Symfony4 project.

Step3: Start the docker images inside your Symfony4 project folder.

cd into your Symfony project folder and type the following command

docker-compose up -d

This command starts the containers. The parameter -d makes them run in the background. If you omit the -d you’ll see the log.

Docker should start building (if it’s the first time for these images) and running with the containers in the background.

Now, open a new chrome tab and type the following URL. The port is the one we set up in the docker-compose.yml – If you check the dictionary of the ngix config, you can see that port 8000 maps the 80, which is the usual webserver port.


You should see it working.

You’re already set up to develop, so happy coding with docker!

Disclaimer: The project container I created has been generated in

The new era of micro frameworks, welcome Symfony 4

Whoop Whoop! Symfony 3.4 and Symfony 4 were released a couple days ago with lots of hype around it!

But what is really different this time from other Symfony versions? Let’s have a look…

Decoupling components

First off, conceptually, Symfony framework is moving towards a more decoupled structure. You probably noticed that instead of creating a new project through the symfony installer, you now use the following command.

composer create-project symfony/skeleton your-project-name

The reason behind this is that in earlier versions, when creating a symfony project, you were installing lots of dependencies and components that you may or may not be using in your application.

The idea now is creating a skeleton type project, and then installing all the components we need for that project seperately. This allows the programer complete freedom to be using whatever component he or she needs.

This in turn means that each Symfony application you code might use different components, even if they were created by Symfony, and therefore you do not need to install all of them every time, which makes each project more light weight!

For example, an API project might not use Twig, so there’s no need to install it in the vendor directory right?

Or I might just install the command component because I need to write some php scripts and I need a quick wrapper to organise my code.

Neither of the projects above would need twig, or form, or entities, or orm… you get the point.

I still remember the days when you had one big bloated Symfony framework installed, and many apps inside it. Inside each app, there were bundles.

After that, it evolved into one app per symfony installation, and each app had it’s different bundles.

It’s now time for the bundle less symfony. Best practices now say that you should have one skeleton per project, and following the decoupling of components, each project will have just the component it needs. This is just great, because every project will now have the underlying code that is really necessary, and not a bloated version of all the Symfony components together.

Having said this, there’s a few components I use 99% of the time when creating websites, so I created this small wrapper script so it’s easy for me to get them all, feel free to grab it and add/remove at your taste :). I call it ‘’

# This script is a wrapper for creating a skeleton and installing the basic components I use most of the time
# To use it: bash symfony-create-project name-of-your-project

if [ "$1" != "" ]; then
	composer create-project symfony/skeleton "$1"
	cd "$1"
	composer require annotations
	composer require --dev profiler
	composer require twig
	composer require orm
	composer require form
	composer require form validator
	echo "parameter expected: name-of-your-project"

Directory Structure

First thing you notice is the disappearance of the web folder. You now have the public folder instead. Inside it we can also see that app.php and app_dev.php have disappeared. Instead, we now have a more default index.php. The environment will be controlled by a .env file in the root directory of the project.

Second of all, the whole Resources folder inside src/ has disapeard as well, and all of it’s contents are in different places. The config files (routing.yml, services.yml…) are in the config/ folder in the root directory. This makes sense now since we have bundle-less applications. All of the twig files have also moved to a general templates/ folder in the root directory.

Now the src/ folder is just for php code, which makes much more sense if you work on a team with frontend and backend dev’s, each of them can now focus on their own folder.

Autoconfiguring and Autowiring

This is my absolute favourite.

Let me just copy this with the comments, as it explains everything really well.

    # default configuration for services in *this* file
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
        public: false       # Allows optimizing the container by removing unused services; this also means
                            # fetching services directly from the container via $container->get() won't work.
                            # The best practice is to be explicit about your dependencies anyway.

Let’s dive into the code.

So I created a new service class,


namespace App\Utils;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;

class NumberGenerator
    public function __construct()

    public function getRandomNumber()
        return  mt_rand(0, 100);

Injected into the action,


namespace App\Controller;

use App\Utils\NumberGenerator;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends Controller
     * @Route("/default/home")
    public function home(NumberGenerator $numberGenerator)
        return $this->render('default/home.html.twig', array(
            'number' => $numberGenerator->getRandomNumber(),

and BANG, it worked like expected.

Let’s recapitulate here… before, we had to create the service, add it in the dependency injection config file, and then get it from the container.

What we just did in Symfony4 is create the service and inject it in the action we needed it. No configuration, no getting… just injection. This REALLY speeds up the process, enough configuration and more coding!!

However, if you don’t want to inject your service in the action, you can still use the container with the get function as we did in the past, although best practices advise not to.


composer require dependency-injection

Secondly, we need to set the service to public.


        public: true

You can now get the service from the container

return $this->render('default/home.html.twig', array(
            'number' => $this->get(NumberGenerator::class)->getRandomNumber(),

There’s another component/bundle, which although is not super necessary, makes your life much much easier: Yup, you guessed it, it’s the Maker Bundle

composer require maker-bundle

This components creates the skeleton files for you (controllers, entities, forms,… etc), for example

bin/console make:controller CarController

More info on the maker bundle here.
More info on all the different component recipes here.


I just love how Symfony keeps giving more and more control to the developer.

Lots uf us (me included) know that changing and evolving things that we’re used to doing is sometimes painful… but after playing with it a little bit, I see this upgrade as a great step forward for better code, better standards and better readability.

So thank you to the Symfony team and everyone who contributed for making this happen!

Happy coding!!

Namespaces and organizing business logic services in Symfony

I want to talk about namespacing services in Symfony, specifically Symfony3.

These are exciting times, Symfony 4 is just round the corner – coming out on November 30th – so this blog post might be irrelevant soon! Nevertheless, concepts are still the same so let´s get into it!

Lately, talking with my team in SlowCode, we defined a common way of defining services.

First rule – using a folder for logic services.

Any service which provides logic to the app would be inside a App\Service folder. This way everything is tidy, and all developers in the team know where to find them.

Second rule – using a folder for the domain name

The next layer is the domain name. This is again, to provide order. You might not think so, but when you end up with 8 domain names, and 2-3 services in each one, then things can get uggly if it’s not tidy 🙂

So for instance, let’s have a service that is related to Stock called StockAvailability, the service would end up with the namespace the folder AppBundle\Service\Stock\StockAvailability

Third rule – using . for folder separation and _ for word separation

The id of the service has to be separated by . when you enter into a new folder, and separated by _ when it’s more than one word.

So in the previous example, we would define the whole thing like so:


        class: AppBundle\Service\Stock\StockAvailability
            - '@doctrine.orm.entity_manager'

Lately in Symfony 3.3, a new way of defining services was brought up.

It´s now a good practice to define the id of the service with the full qualified name. So, instead of defining it like we did before, we would define it like so:


    	public: true
            - '@doctrine.orm.entity_manager'

Declared like this, you can still get the service from the service container (with the new id of course, the full class name)

use AppBundle\Service\Stock\StockAvailability

public function fooAction(Request $request)
    // before Symfony 3.3, you would get it like so
    // $stockService = $this->get('app.service.stock.stock_availability');

    // in Symfony 3.3, you can get it like so
    // This is only available if you defined your service as public
    $stockService = $this->get(StockAvailability::class);

As Symfony’s official page point out, it’s a good practice to define your services as private, not public, and then inject any service you may need in the action inside the controller, instead of getting it from the service container (similar to the dependency injection inside services), for instance

use AppBundle\Service\Stock\StockAvailability

public function fooAction(Request $request, StockAvailability $stockService) { 
    // now we have it injected into our variable $stockService 
    // so we don't need to get it from the container 

So first off, I think the id with the full class name instead of an invented nomenclature is a good thing. At least there will be no more confusion amongst different devs from the team.

About private/public services. I understand where Symfony is going, and I think restricting by ‘injecting’ instead of ‘getting’ makes code more robust, and probably more readable in the end. However, I still think there’s an upside on how things were prior to 3.3 version. Getting services from the container is VERY useful, and provices flexibility and speed.

I think that since you can still define public services, that’s what I’ll keep doing… what will you do?

More info here:
Symfony service container
Symfony 3.3 best practices
Symfony class for service id

Symfony PHPUnit testing database data

Today I had to test some complex algorithm logic we’re writing for a client app. The algorithm I want to test interacts with the DDBB several times through query builders and repositories, so it’s necessary to test this database access, as we need to know the sql queries give the right data also.

It’s the first time I set up a test environment (I worked with Unit Testing before, but never set up the env from scratch). So after reading a bit of doc from the internet, I set up the unit test environment, then wrote some tests that interacted with a test database and test fixtures.

I’ve put together a bit of guide for anyone wanting to get started on this.

Step 1 – Setting a separate database for testing.

Add these lines on your config_test.yml file.



# Doctrine Configuration
        driver:   pdo_mysql
        host:     "%test_database_host%"
        port:     "%test_database_port%"
        dbname:   "%test_database_name%"
        user:     "%test_database_user%"
        password: "%test_database_password%"
        # Workaround for DBAL 2.5 auto-detect -> with server_version allows to doctrine:database:create
        server_version: 5.6 # your database server version here

This will point to the new test database.

Then add the data in parameters.yml (don’t forget parameters.yml.dist for future installs)

# This file is auto-generated during the composer install


    test_database_host: localhost
    test_database_port: null
    test_database_name: test_db_name
    test_database_user: test_db_user
    test_database_password: test_db_pw

Step 2 – Creating a TestCase that we will extend when ddbb fixtures are needed.

This is useful for reusing code, instead of writing it on every file. It also makes all the fixture tests more consistent.

class DataFixtureTestCase extends WebTestCase
    /** @var  Application $application */
    protected static $application;

    /** @var  Client $client */
    protected $client;
    /** @var  ContainerInterface $container */
    protected $container;

    /** @var  EntityManager $entityManager */
    protected $entityManager;

     * {@inheritDoc}
    public function setUp()
        self::runCommand('doctrine:database:drop --force');
        self::runCommand('doctrine:fixtures:load --append --no-interaction --fixtures=tests/AppBundle/DataFixtures/ORM');

        $this->client = static::createClient();
        $this->container = $this->client->getContainer();
        $this->entityManager = $this->container->get('doctrine.orm.entity_manager');


    protected static function runCommand($command)
        $command = sprintf('%s --quiet', $command);

        return self::getApplication()->run(new StringInput($command));

    protected static function getApplication()
        if (null === self::$application) {
            $client = static::createClient();

            self::$application = new Application($client->getKernel());

        return self::$application;
     * {@inheritDoc}
    protected function tearDown()
        self::runCommand('doctrine:database:drop --force');


        $this->entityManager = null; // avoid memory leaks

Most of the code is taken from StackOverflow.

This will create the database and install the fixtures on every test that extends this TestCase. This is important as you want a consistent database with the same data every time!

It’s also important to pass the parameter


This way you can set up all your test fixtures separate from your app fixtures, which will stay clean.

Step 3 – Creating the unit, functional and integration tests

After creating your test fixtures, now you can test your services logic easily by extending the TestCase we just created, like so.

class FooTest extends DataFixtureTestCase
    protected $fooService;

     * {@inheritDoc}
    public function setUp()
        $this->fooService = $this->container->get('');

    public function testFooTrue()

        //custom logic

        $this->assertEquals(true, $value);


And that’s all you need! Happy testing! 🙂

PS: This is a very easy way to get started. For more complex testing environment settings, you can try LiipFunctionalTestBundle

Asking the right questions before building an MVP

This last week we’ve had two potential new clients ask us for an MVP RFQ (Request for quotation).

MVP means Minimal Viable Product, which in the software world means a very simple app that shows your future potential app in a simplified way.

An MVP is normally used to asses future app behaviour, get feedback from users and work from there, and also as a showcase for future investors – investors very rarely invest in ideas, they prefer to see something functional, with customer response and feedback if possible.

It is important that the MVP shows at least a very important feature of your future app. For example, for an ecommerce MPV, logging in a user is not as important as being able to buy a product.

So back to the story, these two clients contacted us and told us what they wanted in a very different way. It became very clear to me that client A knew exactly what he wanted, whilst client B did not. Client A gave direct answers, client B did not.

If a client doesn’t really know what he wants, asking for information becomes a hurdle, ideas seem to jump all around while nothing ever gets done. If you relate to this, you know how frustrating it is… specially because no one is paying for your time at this stage!!

So what’s really important at that moment is to ask the right questions. I cannot stress enough how important this is, because it sets the pace for the whole project.

For example,

Summarize your app in one sentence.
There is a reason why this is number one. This is the most important question – of course they can use more than one sentence, but asking them this puts them in a place where they have to really think of the most important element of the app, not just random features they’re going to add. This is key to understanding properly what your client wants.

What is the reason this app exists?
Spot the USP ‘Unique Selling Point’. There might be none, and this is also fine.

What is the budget for the app and for the MVP?
This will provide you with information of how big this app is going to be, at least at the beginning. The budget might not be a number, it might be a range. This is fine also, because then you’re able to scope the size of the whole project.

How will you make money from the app?
Sometimes clients think that we, as developers, don’t need this question answered, specially at an MVP level. I think it’s useful to know where the focus and priorities are going to be, so you can start thinking big and preparing for the future.

These are some of my non technical preferred questions. Believe me, they do the job right! Once, and only once, you get answers for these, you can start asking the technical ones – maybe that’s for a future post 🙂

Deploy on OVH shared hosting with different PHP versions

Recently we’ve been working on a Symfony project that needed to be deployed on a shared hosting by OVH.

This server was running PHP 5.4 and we could not upgrade it because they had some legacy projects that needed to remain..

So after banging our head quite a few times, we found out that you could create a configuration file so that OVH server reads and reconfigures the settings of the server for that project/folder only.

The file must be called .ovhconfig and it must be on the target folder the DNS points to, so in our case being a Symfony project it should be in the /web folder


PHP 5.6 and PHP 7 on same Mac with Liip

So the other day I wanted to upgrade to php7 but didn’t want to completely remove php56 as I still have legacy projects to maintain and I’m not ‘dockerized’ yet..

You can install php with liip here.

Liip doesn’t overwrite Apple’s php binaries, it installs it under a php5 folder under /usr/local/php5 and then creates the link.

All you now need is this handy bash script to switch from one php version to the other.


#./ php5
#./ php7

#activate php56
if [ "$1" == "php5" ]; then
   sudo rm /usr/local/php5
   sudo ln -s /usr/local/php5-5.6.29-20170114-210819 /usr/local/php5
   sudo pkill php-fpm && sudo php-fpm
   echo "activated php5;"
   sudo apachectl restart
elif [ "$1" == "php7" ]; then
   sudo rm /usr/local/php5
   sudo ln -s /usr/local/php5-7.1.9-20170914-100859 /usr/local/php5
   sudo pkill php-fpm && sudo php-fpm
   echo "activated php7;"
   sudo apachectl restart
   echo "parameter expected: php5 | php7"

PS: You’ll have to obviously adapt the version to the one you installed!

Bundle-less applications with Symfony Flex and Symfony4

So these last few days we’ve had a pretty interesting thread on twitter with @gigo6000 and @WladimirAvila about Symfony’s new directory structure, specially about Bundle Inheritance. You can check the thread here

For the ones like me that still didn’t know or haven’t read it somewhere, bundle inheritance is deprecated from Symfony 3.4 onwards, therefore it makes sense to have bundle-less applications from now on.

It turns out that bundle inheritance was rarely used anyway, except for customizing some templates. Who hasn’t inherited the FOSUserBundle before customizing just the login page? And maybe the entity for some extra attributes?

The question that I ask myself though is how are we going to customize all of this now? Not just templates, but all the config associated, services, entities, forms…

To me, one of the best things about Symfony’s magic was the ability to extend ready made bundles/vendors. Hope there’s still a way to do this!

More info on the Symfony4 directory structure here.

More info on bundle inheritence here

Gedmo Tree in Symfony3

So today at work I’ve had to rethink the way we work with products in an app, and it turns out the best way to face the problem is having a tree based architecture, as products can have sub-products and so on and so forth… so instead of reinventing the wheel, why not use a well established library that uses trees right? Gedmo please!

So it’s actually really easy to install Gedmo extensions to your project.

1) First of all composer install it

php -d memory_limit=-1 /usr/local/bin/composer require stof/doctrine-extensions-bundle

(yes I don’t like playing with my php.ini so for this command I rather pass the memory_limit parameter, also remember to put your composer path correctly, as it won’t read your alias if you pass parameters to php)

2) After that you need to include it to you Kernel

$bundles = [
            // Gedmo
            new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),

3) And the final step is adding the extra configuration in your config.yml

Under the doctrine.orm configuration:

# Doctrine Configuration

        auto_generate_proxy_classes: "%kernel.debug%"
#        naming_strategy: doctrine.orm.naming_strategy.underscore
#        auto_mapping: true

        default_entity_manager: default
              connection: default
              naming_strategy: doctrine.orm.naming_strategy.underscore
              auto_mapping: true
                      ACOS: DoctrineExtensions\Query\Mysql\Acos
                      COS: DoctrineExtensions\Query\Mysql\Cos
                      RADIANS: DoctrineExtensions\Query\Mysql\Radians
                      SIN: DoctrineExtensions\Query\Mysql\Sin
                      type: annotation
                      prefix: Gedmo\Translatable\Entity
                      dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity"
                      alias: GedmoTranslatable # (optional) it will default to the name set for the mapping
                      is_bundle: false
                      type: annotation
                      prefix: Gedmo\Translator\Entity
                      dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translator/Entity"
                      alias: GedmoTranslator # (optional) it will default to the name set for the mapping
                      is_bundle: false
                      type: annotation
                      prefix: Gedmo\Loggable\Entity
                      dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Loggable/Entity"
                      alias: GedmoLoggable # (optional) it will default to the name set for the mappingmapping
                      is_bundle: false
                      type: annotation
                      prefix: Gedmo\Tree\Entity
                      dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Tree/Entity"
                      alias: GedmoTree # (optional) it will default to the name set for the mapping
                      is_bundle: false

Yes, it’s important to remove the auto_mapping as it won’t recognize the key under doctrine.orm

And under the stof_doctrine_extensions configuration:

# Stof Configuration
    default_locale: "%locale%"
    translation_fallback: true
    persist_default_translation: true

    # Only used if you activated the Uploadable extension
        validate_writable_directory: true

        # Default file path: This is one of the three ways you can configure the path for the Uploadable extension
        default_file_path:       "%kernel.root_dir%/../web/uploads"

        # Mime type guesser class: Optional. By default, we provide an adapter for the one present in the HttpFoundation component of Symfony
        mime_type_guesser_class: Stof\DoctrineExtensionsBundle\Uploadable\MimeTypeGuesserAdapter

        # Default file info class implementing FileInfoInterface: Optional. By default we provide a class which is prepared to receive an UploadedFile instance.
        default_file_info_class: Stof\DoctrineExtensionsBundle\Uploadable\UploadedFileInfo
            translatable:   true
            blameable:      false
            timestampable:  true
            tree:           true
            uploadable:     false
            sluggable:      true

That’s it! You’re set, super easy right?

Now let’s go to the entity, in my case it’s the Product. Don’t forget to import the Gedmo annotation and then add the annotation to the class, like so:

use Gedmo\Mapping\Annotation as Gedmo;

 * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
 * ...
 * @Gedmo\Tree(type="nested")
class Product
     * @Gedmo\TreeLeft
     * @ORM\Column(name="lft", type="integer")
    private $lft;

     * @Gedmo\TreeLevel
     * @ORM\Column(name="lvl", type="integer")
    private $lvl;

     * @Gedmo\TreeRight
     * @ORM\Column(name="rgt", type="integer")
    private $rgt;

     * @Gedmo\TreeRoot
     * @ORM\ManyToOne(targetEntity="Product")
     * @ORM\JoinColumn(name="tree_root", referencedColumnName="id", onDelete="CASCADE")
    private $root;

     * @Gedmo\TreeParent
     * @ORM\ManyToOne(targetEntity="Product", inversedBy="children")
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE")
    private $parent;

     * @ORM\OneToMany(targetEntity="Product", mappedBy="parent")
     * @ORM\OrderBy({"lft" = "ASC"})
    private $children;


Running the command

bin/console doc:gen:entities AppBundle:Product

will generate all the getters and setters, pretty handy…

And now the repository:

use Gedmo\Tree\Entity\Repository\NestedTreeRepository;

 * ProductRepository
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
class ProductRepository extends NestedTreeRepository

The NestedTreeRepository has very useful functions, play with them before making your custom ones, cause they’ve already thought with pretty much everything, for instance the function childrenHierarchy() gives you the nested array, with decoration output if you pass parameters as options. You can find more info in the RepositoryInterface and RepositoryUtilsInterface from Gedmo\Tree namespace.

PS: Don’t forget to run your schema update command

bin/console doc:sch:update --force

Now you focus on what really matters, you project’s logic.

Happy coding! 🙂