Doctrine Entities in a custom database

Here's how you can use Doctrine entities located in a database that's different from the default Concrete CMS one.

Tell Concrete to stay away from your own entities

First of all, you need to be sure that Concrete doesn't add your entities to the standard entity set it manages.

In your package controller.php add this line right after the namespace declaration:

use Concrete\Core\Database\EntityManager\Provider\ProviderInterface;

and declare that the package controller implements the ProviderInterface interface:

class Controller extends Package implements ProviderInterface

finally, define a new getDrivers method that returns an empty array:

public function getDrivers()
{
    return [];
}

NOTE: if you also have Doctrine entities that should be managed by Concrete, you need to keep the two kinds of entities in two distict directories. For instance, you may have in src/Entities/Standard the entities that should be fully handled by Concrete, and in src/Entities/Custom the entities that live in a different database. In this case, the getDrivers method should return a DriverInterface instance that tells Concrete to use the entities in the src/Entities/Standard directory.

Create your own connection

Next you have to define the connection to the database that contains your entities. See this documentation page to know how to add it. Let's allume that you'll name the connection as my-connection.

Create your own Entity Manager

You can define your own entity manager in the src directory of your package.

<?php

namespace MyNamespace;

use Concrete\Core\Database\DatabaseStructureManager;
use Doctrine\ORM\EntityManager;

class MyEntityManager extends EntityManager
{
    /**
     * Refresh the proxy classes.
     */
    public function refreshProxyClasses()
    {
        $manager = new DatabaseStructureManager($this);
        $manager->clearCacheAndProxies();
    }
}

The refreshProxyClasses method proposed here is just an example. If executed, it will only refresh the doctrine proxy classes without touching the database structure (it's very useful if your database structure is shared between other projects and it must not be changed by Concrete).

Initialize your own Entity Manager

Then, in your package controller.php, you can add a function that initializes your the entity manager:

private function registerEntityManager()
{
    $this->app->singleton(\MyNamespace\MyEntityManager::class, function ($app) {
        $driver = new AnnotationDriver($app->make('orm/cachedAnnotationReader'), [DIR_BASE . '/' . DIRNAME_PACKAGES . '/my_package_handle/src/Entity']);
        $driverChain = new MappingDriverChain();
        $driverChain->addDriver($driver, 'MyNamespace\Entity');
        $ormConfiguration = $app->make(\Doctrine\ORM\Configuration::class);
        $ormConfiguration->setMetadataDriverImpl($driverChain);
        $databaseManager = $app->make(\Concrete\Core\Database\DatabaseManager::class); 
        $connection = $databaseManager->connection('my-connection');

        $entityManager = new \MyNamespace\MyEntityManager($connection, $ormConfiguration, $connection->getEventManager());

        return $entityManager;
    });
}

public function on_start()
{
    $this->registerEntityManager();
}

In this example, the custom entity manager will use the standard annotation driver, and the entities will be placed in the src/Entity folder under your package.

Proxy class generation

When your package is installed or upgraded, you may need to create or refresh the proxy classes. In order to do so, simply add these two method to your package controller.php:

public function install()
{
    $this->registerEntityManager();
    $this->refreshProxyClasses();
}

public function upgrade()
{
    $this->registerEntityManager();
}

private function registerEntityManager()
{
    $this->app->make(\MyNamespace\MyEntityManager::class)->refreshProxyClasses();
}

Integration with Concrete

Since Concrete 8.3, it's possible to list your own Doctrine entities in the /dashboard/system/environment/entities dashboard page, as well as refreshing the proxy classes when users hit the Refresh Entities in that page.

Just add this method to your package controller.php file and call it from the on_start method:

private function registerEntityEvents()
{
    $app = $this->app;
    $director = $app->make(\Symfony\Component\EventDispatcher\EventDispatcher::class);
    $director->addListener('on_list_package_entities', function ($event) use ($app) {
        $event->addEntityManager($app->make(\MyNamespace\MyEntityManager::class));
    });
    $director->addListener('on_refresh_package_entities', function ($event) use ($app) {
        $app->make(\MyNamespace\MyEntityManager::class)->refreshProxyClasses();
    });
}