Including Doctrine Entities in Your Package (Version 8+)

Requirements

This approach requires that a developer be very familiar with Doctrine 2 and its ORM component.

Standard Behavior

By default, entities in packages are provided in the following way:

  • Using the Annotation Driver, which loads database information from annotations provided directly in PHP code.
  • If the legacy \Src namespace is enabled in a package, which is the case if the package doesn't use any custom autoloaders, and supports Concrete CMS prior to version 8, then the entire packages/package_handle/src/ directory is scanned for entities and their annotations.
  • If the legacy \Src namespace is not enabled, then the class locations specified by $pkg->getPackageAutoloaderRegistries(), and any files found in packages/your_package/src/Concrete/Entity are scanned for entities.

This behavior can all be found in Concrete\Core\Database\EntityManager\Provider\DefaultPackageProvider, which is the default entity manager provider class used if another one isn't provided.

Annotation Driver Functionality

Much of the documentation found in the earlier Doctrine ORM package documentation is still accurate, with one large change:

If your package contains entity classes that use annotations, and the package requires version 8 or greater, you must include @ORM in your annotations. So instead of this:

<?php

namespace Concrete\Package\Statistics\Src\Entity;
/**
 * @Entity
 * @Table(name="StatisticsUserAgents")
 */
class UserAgent
{

    /**
     * @Id @Column(type="integer")
     * @GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @Column(type="string")
     */
    protected $value;

    /**
     * @Column(type="string")
     */
    protected $hash;

}

You must import the ORM namespace, and preface all annotations with ORM:

<?php

namespace Concrete\Package\Statistics\Src\Entity;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="StatisticsUserAgents")
 */
class UserAgent
{

    /**
     * @ORM\Id @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     */
    protected $value;

    /**
     * @ORM\Column(type="string")
     */
    protected $hash;

}

Why did we do this? Not just to make your life more difficult. Once the @ORM namespace is included, it makes it so that your Doctrine can do much, much more. For example, any packages that run on version 8 will be able to support Doctrine Extensions and more.

Customizable Behavior

It's easy to customize the behavior of the package entity manager. What can you do with a customizable entity manager configuration?

  1. If you have one specific portion of your package that contains entities, and you don't want your entire package's code base scanned for entities, provide a custom package provider that points just to the spots in the filesystem that contain your entity classes.
  2. Provide additional Doctrine types or extensions
  3. Provide your entity information in alternate formats, like XML or YAML (instead of annotations in PHP classes.)

To customize the way your package configures the entities it provides, do one of the following

  1. Make your package's controller implement the Concrete\Core\Database\EntityManager\Provider\ProviderAggregateInterface, which will be responsible for implementing the getEntityManagerProvider() method, which returns an object of the Concrete\Core\Database\EntityManager\Provider\ProviderInterface interface,
  2. Make your package's controller implement the Concrete\Core\Database\EntityManager\Provider\ProviderInterface method, which which returns an array of Concrete\Core\Database\EntityManager\Driver\DriverInterface objects.

In both cases, you'll ultimately be providing at least one Concrete\Core\Database\EntityManager\Driver\DriverInterface object.

Example

Here's a simple example. Let's say you have a package that contains entities in the following locations:

  • Concrete\Package\YourPackage\Entity (which is automatically loaded by Concrete from packages/your_package/src/Concrete/Entity)
  • PortlandLabs\Testing\Entity, which you have set up as a custom class location using $pkgAutoloaderRegistries:

    protected $pkgAutoloaderRegistries = array( 'src/Testing' => '\PortlandLabs\Testing', );

Instead of scanning all files found in packages/your_package/src/ (which is the default, standard behavior), you just want to load classes using the standard annotation driver from these two locations. First, modify your package controller so that it implements the ProviderAggregateInterface, and import the interface, as well as the StandardPackageProvider class:

use Concrete\Core\Database\EntityManager\Provider\ProviderAggregateInterface;
use Concrete\Core\Database\EntityManager\Provider\StandardPackageProvider;
class Controller extends Package implements ProviderAggregateInterface

Then, implement the provider class:

public function getEntityManagerProvider()
{
    $provider = new StandardPackageProvider($this->app, $this, [
        'src/Concrete/Entity' => 'Concrete\Package\YourPackage\Entity',
        'src/Testing/Entity' => 'PortlandLabs\Testing\Entity'
    ]);
    return $provider;
}

That's it! The standard package provider is a simple library that most classes that want to use annotated entities but load them from custom locations can use.

Other File Formats

Other classes exist to provide the entity definitions in different formats. Check out Concrete\Core\Database\EntityManager\Provider\YamlProvider and Concrete\Core\Database\EntityManager\Provider\XmlProvider if you want to provide your package entities in YAML or XML formats.