3. Usage

3.1. How to Provide Model Classes for several Doctrine Implementations

When building a bundle that could be used not only with Doctrine ORM but also the CouchDB ODM, MongoDB ODM or PHPCR ODM, you should still only write one model class. The Doctrine bundles provide a compiler pass to register the mappings for your model classes. This bundle helps you easily register these mappings.

Note

See Symfony documentation for more details.

Let’s say you created a new bundle called ContentBundle and you want to register your model classes mappings. You could do that in a normal way by defining it in the configuration file as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# app/config/config.yml

doctrine:
    # ..
    orm:
        entity_managers:
            default:
                # ..
                auto_mapping: false
                mappings:
                    AcmeContentBundle:
                        type: yml
                        prefix: Acme\ContentBundle\Model
                        dir: Resources/config/doctrine

If you want to have a flexible way to register your model classes mappings both for Doctrine ORM and PHPCR ODM using a single model class, extend your class with SWP\Bundle\StorageBundle\DependencyInjection\Bundle\Bundle from StorageBundle.

Note

SWPStorageBundle is able to load mappings in XML, YAML and annotation formats. By default YAML mappings files are loaded. You can change this by setting the $mappingFormat property to: protected $mappingFormat = BundleInterface::MAPPING_XML; (see SWP\Component\Storage\Bundle\BundleInterface) if you wish to load mapping files in XML format.

For example, to extend the AcmeContentBundle, you could use the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/Acme/ContentBundle/AcmeContentBundle.php
namespace Acme\ContentBundle\AcmeContentBundle;

use SWP\Bundle\StorageBundle\DependencyInjection\Bundle\Bundle;
use SWP\Bundle\StorageBundle\Drivers;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AcmeContentBundle extends Bundle
{
    /**
     * {@inheritdoc}
     */
    public function getSupportedDrivers()
    {
        return [
            Drivers::DRIVER_DOCTRINE_ORM,
            Drivers::DRIVER_DOCTRINE_PHPCR_ODM,
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function getModelClassNamespace()
    {
        return 'Acme\\ContentBundle\\Model';
    }
}

Your bundle can now support multiple drivers. According to the example above, your Article model class namespace is specified in the getModelClassNamespace() method. Two drivers are configured: PHPCR and ORM. In this case, you can create a model class for PHPCR and ORM and extend the default Acme\ContentBundle\Model\Article class. You would then have different implementations for PHPCR and ORM using the same model class:

  • Acme\ContentBundle\ODM\PHPCR\Article should extend Acme\ContentBundle\Model\Article
  • Acme\ContentBundle\ORM\Article should extend Acme\ContentBundle\Model\Article

All that you need to do now is to place the mapping files for each model classes. In this case the Acme\ContentBundle\ODM\PHPCR\Article class mapping should be placed inside the Resources/config/doctrine-phpcr directory. The mappings for ORM classes should be placed inside the Resources/config/doctrine-orm directory.

Note

A reference to the directories where mapping files should be placed for model classes is generated automatically, based on the supported driver. In the case of PHPCR it will be Resources/config/doctrine-phpcr and in the case of Doctrine ORM it will be Resources/config/doctrine-orm. These directories should be created manually if they don’t exist already.

The getSupportedDrivers defines supported drivers by the ContentBundle e.g. PHPCR ODM, MongoDB ODM etc. You should use the SWP\Bundle\StorageBundle\Drivers class to specify the supported drivers, as shown in the example above.

The Drivers class provides drivers’ constants:

1
2
3
4
5
6
7
8
namespace SWP\Bundle\StorageBundle;

class Drivers
{
    const DRIVER_DOCTRINE_ORM = 'orm';
    const DRIVER_DOCTRINE_MONGODB_ODM = 'mongodb';
    const DRIVER_DOCTRINE_PHPCR_ODM = 'phpcr';
}

3.2. How to automatically register Services required by the configured Storage Driver

This bundle enables you to register required services on the basis of the configured storage driver, to standardize the definitions of registered services.

By default, this bundle registers:

  • repository services
  • factory services
  • object manager services
  • parameters

Based on the provided model class it will register the default factory and repository, where you will be able to create a new object based on the provided model class name, adding or removing objects from the repository.

3.2.1. Set Configuration for your Bundle

Let’s start from your bundle configuration, where you will need to specify the default configuration. In this example let’s assume you already have a bundle called ContentBundle and you want to have working services to manage your resources.

The default Configuration class would look something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace Acme\ContentBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $treeBuilder->root('acme_content');

        return $treeBuilder;
    }
}

Now, let’s add the configuration for the Acme\ContentBundle\ODM\PHPCR\Article model class, for the PHPCR driver:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php

namespace Acme\ContentBundle\DependencyInjection;
// ..

use Acme\ContentBundle\ODM\PHPCR\Article;

class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $treeBuilder->root('acme_content')
            ->children()
                ->arrayNode('persistence')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->arrayNode('phpcr')
                            ->addDefaultsIfNotSet()
                            ->canBeEnabled()
                            ->children()
                                ->arrayNode('classes')
                                    ->addDefaultsIfNotSet()
                                    ->children()
                                        ->arrayNode('article')
                                            ->addDefaultsIfNotSet()
                                            ->children()
                                                ->scalarNode('model')->cannotBeEmpty()->defaultValue(Article::class)->end()
                                                ->scalarNode('repository')->defaultValue(null)->end()
                                                ->scalarNode('factory')->defaultValue(null)->end()
                                                ->scalarNode('object_manager_name')->defaultValue(null)->end()
                                            ->end()
                                        ->end()
                                    ->end()
                                ->end()
                            ->end()
                        ->end() // phpcr
                    ->end()
                ->end()
            ->end();

        return $treeBuilder;
    }
}

Note

The repository, factory and object_manager_name nodes are configured to use null as the default value. It means that the default factory, repository and object manager services will be registered in the container.

3.2.2. Register configured classes in your Extension class

Now that you have the configuration defined, it is time to register those classes using the Extension class in your bundle. By default, this class is generated inside the DependencyInjection folder in every Symfony Bundle.

In this ContentBundle example it will be located under the namespace Acme\ContentBundle\DependencyInjection. The fully qualified class name will be Acme\ContentBundle\DependencyInjection\AcmeContentExtension.

You need to extend this class by the SWP\Bundle\StorageBundle\DependencyInjection\Extension\Extension class, which will give you access to register configured classes needed by the storage. The registerStorage method will do the whole magic for you. See the code below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

namespace Acme\ContentBundle\DependencyInjection;

// ..
use SWP\Bundle\StorageBundle\Drivers;
use SWP\Bundle\StorageBundle\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader;

class AcmeContentExtension extends Extension
{
    /**
     * {@inheritdoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $config = $this->processConfiguration(new Configuration(), $configs);
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yml');

        if ($config['persistence']['phpcr']['enabled']) {
            $this->registerStorage(Drivers::DRIVER_DOCTRINE_PHPCR_ODM, $config['persistence']['phpcr'], $container);
        }
    }
}

If the PHPCR persistence backend is enabled, it will register the following services in the container:

Service ID Class name
swp.factory.article SWP\Bundle\StorageBundle\Factory\Factory
swp.repository.article.class | Acme\ContentBundle\PHPCR\Article
swp.repository.article SWP\Bundle\StorageBundle\Doctrine\ODM\PHPCR\DocumentRepository

together with all parameters:

Parameter Name Value
swp.factory.article.class SWP\Bundle\StorageBundle\Factory\Factory
swp.model.article.class Acme\ContentBundle\PHPCR\Article
swp.repository.article.class SWP\Bundle\StorageBundle\Doctrine\ODM\PHPCR\DocumentRepository

If your configuration supports Doctrine ORM instead of PHPCR, the default service definitions would be:

Service ID Class name
swp.factory.article SWP\Bundle\StorageBundle\Factory\Factory
swp.object_manager.article alias for “doctrine.orm.default_entity_manager”
swp.repository.article SWP\Bundle\StorageBundle\Doctrine\ORM\EntityRepository

And all parameters in the container would look like:

Parameter Name Value
swp.factory.article.class SWP\Bundle\StorageBundle\Factory\Factory
swp.model.article.class Acme\ContentBundle\ORM\Article
swp.repository.article.class SWP\Bundle\StorageBundle\Doctrine\ORM\EntityRepository

You could then access parameters from the container, as visible below:

1
2
3
4
<?php
//..
$className = $container->getParameter('swp.model.article.class');
var_dump($className); // will return Acme\ContentBundle\PHPCR\Article

Now, register all classes in the configuration file:

1
2
3
4
# app/config/config.yml
swp_content:
    persistence:
        phpcr: true

The above configuration is equivalent to:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# app/config/config.yml
swp_content:
    persistence:
        phpcr:
            enabled: true
            classes:
                article:
                    model: Acme\ContentBundle\ODM\PHPCR\Article
                    factory: ~
                    repository: ~
                    object_manager_name: ~

3.2.3. How to create and use custom repository service for your model

For some use cases you would need to implement your own methods in the repository, like findOneBySlug() or findAllArticles(). It’s very easy!

You need to create your custom implementation for the repository. In this example you will create a custom repository for the Article model class and Doctrine PHPCR persistence backend.

Firstly, you need to create your custom repository interface. Let’s name it ArticleRepositoryInterface and extend it by the SWP\Component\Storage\Repository\RepositoryInterface interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace Acme\ContentBundle\PHPCR;

use Acme\ContentBundle\Model\ArticleInterface;
use SWP\Component\Storage\Repository\RepositoryInterface;

interface ArticleRepositoryInterface extends RepositoryInterface
{
    /**
     * Find one article by slug.
     *
     * @param string $slug
     *
     * @return ArticleInterface
     */
    public function findOneBySlug($slug);

    /**
     * Find all articles.
     *
     * @return mixed
     */
    public function findAllArticles();
}

Secondly, you need to create your custom repository class. Let’s name it ArticleRepository and implement the ArticleRepositoryInterface interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace Acme\ContentBundle\PHPCR;

use Acme\ContentBundle\Model\ArticleRepositoryInterface;
use SWP\Bundle\StorageBundle\Doctrine\ODM\PHPCR\DocumentRepository;

class ArticleRepository extends DocumentRepository implements ArticleRepositoryInterface
{
    /**
     * {@inheritdoc}
     */
    public function findOneBySlug($slug)
    {
        return $this->findOneBy(['slug' => $slug]);
    }

    /**
     * {@inheritdoc}
     */
    public function findAllArticles()
    {
        return $this->createQueryBuilder('o')->getQuery();
    }
}

Note

If you want to create a custom repository for the Doctrine ORM persistence backend, you need to extend your custom repository class by the SWP\Bundle\StorageBundle\Doctrine\ORM\EntityRepository class.

The last step is to add your custom repository to the configuration file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# app/config/config.yml
swp_content:
    persistence:
        phpcr:
            enabled: true
            classes:
                article:
                    model: Acme\ContentBundle\ODM\PHPCR\Article
                    factory: ~
                    repository: Acme\ContentBundle\PHPCR\ArticleRepository
                    object_manager_name: ~

Note

Alternatively, you could add it directly in your Configuration class.

Note

You can change repository class by simply changing your bundle configuration, without needing to change the code.

3.2.4. How to create and use custom factory service for your model

You may need to have a different way of creating objects than the default way of doing it. Imagine you need to create an Article object with the route assigned by default.

Note

In this example you will create a custom factory for your Article object and Doctrine PHPCR persistence backend.

Let’s create a custom interface for your factory. Extend your custom class by the SWP\Component\Storage\Factory\FactoryInterface class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Acme\ContentBundle\Factory;

use SWP\Bundle\ContentBundle\Model\ArticleInterface;
use SWP\Component\Bridge\Model\PackageInterface;
use SWP\Component\Storage\Factory\FactoryInterface;

interface ArticleFactoryInterface extends FactoryInterface
{
    /**
     * Create a new object with route.
     *
     * @param string $route
     *
     * @return ArticleInterface
     */
    public function createWithRoute($route);
}

Create the custom Article factory class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php

namespace Acme\ContentBundle\Factory;

use SWP\Component\Storage\Factory\FactoryInterface;

class ArticleFactory implements ArticleFactoryInterface
{
    /**
     * @var FactoryInterface
     */
    private $baseFactory;

    /**
     * ArticleFactory constructor.
     *
     * @param FactoryInterface $baseFactory
     */
    public function __construct(FactoryInterface $baseFactory)
    {
        $this->baseFactory = $baseFactory;
    }

    /**
     * {@inheritdoc}
     */
    public function create()
    {
        return $this->baseFactory->create();
    }

    /**
     * {@inheritdoc}
     */
    public function createWithRoute($route)
    {
        $article = $this->create();
        // ..
        $article->setRoute($route);

        return $article;
    }
}

Create a compiler pass to override the default Article factory class with your custom factory on container compilation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

namespace Acme\ContentBundle\DependencyInjection\Compiler;

use SWP\Component\Storage\Factory\Factory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Parameter;

class RegisterArticleFactoryPass implements CompilerPassInterface
{
    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('swp.factory.article')) {
            return;
        }

        $baseDefinition = new Definition(
            Factory::class,
            [
                new Parameter('swp.model.article.class'),
            ]
        );

        $articleFactoryDefinition = new Definition(
            $container->getParameter('swp.factory.article.class'),
            [
                $baseDefinition,
            ]
        );

        $container->setDefinition('swp.factory.article', $articleFactoryDefinition);
    }
}

Don’t forget to register your new compiler pass in your Bundle class (AcmeContentBundle):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php

use Acme\ContentBundle\DependencyInjection\Compiler\RegisterArticleFactoryPass;
// ..

/**
 * {@inheritdoc}
 */
public function build(ContainerBuilder $container)
{
    parent::build($container);
    $container->addCompilerPass(new RegisterArticleFactoryPass());
}

The last thing required to make use of your new factory service is to add it to the configuration file, under the factory node:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# app/config/config.yml
swp_content:
    persistence:
        phpcr:
            enabled: true
            classes:
                article:
                    model: Acme\ContentBundle\ODM\PHPCR\Article
                    factory: Acme\ContentBundle\Factory\ArticleFactory
                    repository: ~
                    object_manager_name: ~

Note

Alternatively, you could add it directly in your Configuration class.

You would then be able to use the factory like so:

1
2
3
$article = $this->get('swp.factory.article')->createWithRoute('some-route');
// or create flat object
$article = $this->get('swp.factory.article')->create();

Note

You can change factory class by simply changing your bundle configuration, without needing to change the code.

3.2.5. Configuring object manager for your model

As you can see, there is the object_manager_name option in the Configuration class, which is the default Object Manager (Contract for a Doctrine persistence layer) name.

In the case of Doctrine ORM it’s doctrine.orm.default_entity_manager, in PHPCR it’s doctrine_phpcr.odm.default_document_manager.

If you set this option to be, for example, test the doctrine.orm.test_entity_manager object manager service’s id will be used. Of course this new test document, in the case of PHPCR, should be first configured in the Doctrine PHPCR Bundle as described in the bundle documentation on multiple document managers. For Doctrine ORM it should be configured as shown in the Doctrine ORM Bundle documentation on multiple entity managers.

The possibility of defining a default Object Manager for a Doctrine persistence layer, and making use of it in the registered repositories and factories in your Bundle, is very useful in case you are using different databases or even different sets of entities.

Note

Factories and repositories are defined as a services in Symfony container to have better flexibility of use.

3.3. Resolve target entities

This chapter is strictly related to How to Define Relationships with Abstract Classes and Interfaces so please read it first.

This functionality allows you to define relationships between different entities without making them hard dependencies. All you need to do is to define interface node in your bundle’s Configuration class.

See example below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php

namespace Acme\Bundle\CoreBundle\DependencyInjection;
// ..

use Acme\Component\MultiTenancy\Model\Tenant;
use Acme\Component\MultiTenancy\Model\TenantInterface;
use Acme\Component\MultiTenancy\Model\Organization;
use Acme\Component\MultiTenancy\Model\OrganizationInterface;

class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $treeBuilder->root('acme_core')
            ->children()
                ->arrayNode('persistence')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->arrayNode('phpcr')
                            ->addDefaultsIfNotSet()
                            ->canBeEnabled()
                            ->children()
                                ->arrayNode('classes')
                                    ->addDefaultsIfNotSet()
                                    ->children()
                                        ->arrayNode('tenant')
                                            ->addDefaultsIfNotSet()
                                            ->children()
                                                ->scalarNode('model')->cannotBeEmpty()->defaultValue(Tenant::class)->end()
                                                ->scalarNode('interface')->cannotBeEmpty()->defaultValue(TenantInterface::class)->end()
                                                ->scalarNode('repository')->defaultValue(null)->end()
                                                ->scalarNode('factory')->defaultValue(null)->end()
                                                ->scalarNode('object_manager_name')->defaultValue(null)->end()
                                            ->end()
                                        ->end()
                                        ->arrayNode('organization')
                                            ->addDefaultsIfNotSet()
                                            ->children()
                                                ->scalarNode('model')->cannotBeEmpty()->defaultValue(Organization::class)->end()
                                                ->scalarNode('interface')->cannotBeEmpty()->defaultValue(OrganizationInterface::class)->end()
                                                ->scalarNode('repository')->defaultValue(null)->end()
                                                ->scalarNode('factory')->defaultValue(null)->end()
                                                ->scalarNode('object_manager_name')->defaultValue(null)->end()
                                            ->end()
                                        ->end()
                                    ->end()
                                ->end()
                            ->end()
                        ->end() // phpcr
                    ->end()
                ->end()
            ->end();

        return $treeBuilder;
    }
}

In this case you will be able to specify your interface for your model via config file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# app/config/config.yml
swp_content:
    persistence:
        phpcr:
            enabled: true
            classes:
                tenant:
                    model: Acme\Bundle\CoreBundle\Model\Tenant # extends default Acme\Component\MultiTenancy\Model\Tenant class
                    interface: ~
                    # ..
                organization:
                    model: Acme\Bundle\CoreBundle\Model\Organization # extends default Acme\Component\MultiTenancy\Model\Organization class
                    interface: ~
                    # ..
Now, no matter which model (implementing for example Acme\Component\MultiTenancy\Model\OrganizationInterface)
you will use in your bundle’s configuration above, the interface will be automatically resolved to defined entity and will be used by your mapping file without a need to change any extra code or configuration setup.

The above is equivalent to if the Tenant has a relation to Organization and vice versa.

1
2
3
4
5
6
7
8
# app/config/config.yml
doctrine:
    # ...
    orm:
        # ...
        resolve_target_entities:
            Acme\Component\MultiTenancy\Model\OrganizationInterface: Acme\Bundle\CoreBundle\Model\Organization
            Acme\Component\MultiTenancy\Model\TenantInterface: Acme\Bundle\CoreBundle\Model\Tenant

In this example above every time you will want to change your model inside your bundle’s configuration you would also need to care about the Doctrine config as the specified entity will not change automatically to a new one which was defined in bundle’s config.

3.4. Inheritance Mapping

By default every entity inside bundle should be mapped as Mapped superclass. This bundle helps you manage and simplify inheritance mapping in case you want to use default mapping or extend it. In this case the following applies:

  • If you do not configure your custom class, the default mapped superclasses become entites.
  • Otherwise they become mapped superclasses and move the conflicting mappings (these which you cannot normally configure on mapped superclass) to your class mapping. For example, you do not need anymore to map Organization -> Tenants inside your custom class, it is copied transparently from the bundle.
  • It also works on all levels, so you can cleanly override the core bundle models! If you configure other class than core one, your entity will be used and the core model will remain mapped superclass.

Note

This feature and its description has been ported from Sylius project. See related issue.