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 extendAcme\ContentBundle\Model\Article
Acme\ContentBundle\ORM\Article
should extendAcme\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.