symfonydoctrinedoctrine-odmodm

In Symfony2 with Doctrine ODM how can I dynamically create fields in a document?


I have an Site Record that can contain any number of AdProvider configuration fields with differing information. Unfortunately, the fieldNames (names of the providers) are unique and there will be more coming. I could just hardcode each of them in the Document as hash types, but I would have to update the Document each time a new Provider is added.

I'd like to dynamically modify the Document itself looking at the list of Providers that I can get from another Mongo collection, but I can't figure out how to do this.

My first attempt was to create a listener on the loadClassMetaData event and map the new fields. I'm seeing the field mappings but they aren't reflected in the document. Obviously there aren't any getters and setters for these fields so I tried accessing them with the magic __get and __set methods, but I get errors that they don't exist.

Maybe I'm going about this the wrong way?

Example Mongo record:

{
    "_id" : ObjectId("4ff1d29d99c6667722000000"),
    "_type" : [
        "Models_Site"
    ],
    "enabledAdProviders" : [
        "provider1",
        "provider2",
        "provider3",
        "provider4"
    ],
    "provider1" : {
        "id" : "4028cbff38e2d7c00666fd2fdc770208"
    },
    "provider2" : {
        "placements" : {
            "Top_300x50" : "477",
            "Btm_300x50" : "478",
            "Top_320x50" : "477",
            "Btm_320x50" : "478"
        }
    },
    "provider3" : {
        "id" : "8a809449013331fdcdc6662708532b20"
    },
    "siteId" : "PsTl",
    "siteName" : "Publisher Site",
    "provider4" : {
        "placements" : {
            "Top_300x50" : "430",
            "Btm_300x50" : "430"
        }
    }
}

My listener:

<?php
namespace BIM\DataBundle\Listener;

use BIM\DataBundle\Document\AdPublisherRecord;
use BIM\DataBundle\Document\AdProviderRecord;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;

class AdPublisherSiteSetup
{
private $serviceContainer;

/**
 * This service is called every time Ads doctrine odm loads a document. 
 * We are dynamically creating the ad provider setting nodes on the AdPublisher Record 
 * 
 */
public function __construct($serv){
    $this->serviceContainer = $serv;
}

public function loadClassMetadata(\Doctrine\ODM\MongoDB\Event\LoadClassMetadataEventArgs $args)
{
    $metaData = $args->getClassMetadata();
    $document = (string)$metaData->getName();

    if($document == "BIM\DataBundle\Document\AdPublisherRecord"){
        //query for ad providers
        //create as a hash type to store each providers settings.
        $providerList = $this->serviceContainer->get('ads.publisher.factory')->getProviderList();
        foreach ($providerList as $name => $value) {
            $metaData->mapField(array('fieldName' => $name, 'type' => 'hash'));
        }   
    }
}
}

Solution

  • You can't do this with plain Doctrine-odm, however I've used a simple trick to do almost the same:


    Since MongodDB Documents can be nested, simply create an @Hash property in your doc, that will contains the schema-less properties (think of it as an hash-bag):

    class Doc {
    
       /** 
        * Schema-less dynamic properties goes here:
        * @MongoDB\Hash 
        */
       public $extraFields;
    
       /** @MongoDB\Date */
       public $createdDate;
    
       //Other static properties...
    }
    

    The only caveat is that the schema-less properties will go in the $extraFields property, and not on the root.

    However this is only an aesthetic problem, you can do everything on those fields (indexes, queries, map reduce, ecc)

    In this way you can have either statically defined properties (like Id, createdDate, ecc) and an hash bag of dynamic properties.

    If you want to document the existence of some of those dynamic properties (and avoid common typo problems to your colleagues), you can define some access methods in the Doc:

    class Doc {
    
        public getExtraName() {
            if(empty($this->extraFields['name'])) return null;
             return $this->extraFields['name'];
        }
    }