3

There are lots of articles regarding factory method implementation in PHP. I want to implement such a method for my MongoDB implementation in PHP.

I wrote the code something like below. Please Look at that code.

<?php
class Document {

    public $value = array();

    function __construct($doc = array()) {        
        $this->value = $doc;
    }

    /** User defined functions here **/
}

class Collection extends Document {
    //initialize database    
    function __construct() {            
        global $mongo;        
        $this->db = Collection::$DB_NAME;
    }

    //select collection in database
    public function changeCollection($name) {
        $this->collection = $this->db->selectCollection($name);
    }

    //user defined method
    public function findOne($query = array(), $projection = array()) {
        $doc = $this->collection->findOne($query, $projection);
        return isset($doc) ? new Document($doc) : false;
    }

    public function find($query = array(), $projection = array()) {
        $result = array();
        $cur = $this->collection->find($query, $projection);

        foreach($cur as $doc) {
            array_push($result, new Document($doc));
        }

        return $result;
    }

    /* Other user defined methods will go here */
}

/* Factory class for collection */

class CollectionFactory {
    private static $engine;

    private function __construct($name) {}    
    private function __destruct() {}
    private function __clone() {}

    public static function invokeMethod($collection, $name, $params) {
        static $initialized = false;

        if (!$initialized) {
            self::$engine = new Collection($collection);
            $initialized = true;
        }

        self::$engine->changeCollection($collection);

        return call_user_func_array(array(self::$engine, $name), $params);
    }
}

/* books collection */
class Books extends CollectionFactory {    
    public static function __callStatic($name, $params) {
        return parent::invokeMethod('books', $name, $params);
    }
}

/* authors collection */
class Authors extends CollectionFactory {    
    public static function __callStatic($name, $params) {
        return parent::invokeMethod('authors', $name, $params);
    }
}

/* How to use */

$books = Books::findOne(array('name' => 'Google'));
$authors = Authors::findOne(array('name' => 'John'));

Authors::update(array('name' => 'John'), array('name' => 'John White'));
Authors::remove(array('name' => 'John'));
?>

My questions are:-

  1. Is this correct PHP implementation of Factory method?
  2. Does this implementation have any issues?
  3. Are there any better methodologies over this for this scenario?

Thanks all for the answers.

user10
  • 5,186
  • 8
  • 43
  • 64

2 Answers2

2
  1. Hmm no, because with your piece of code you make ALL methods on the collection class available for a static call. That's not the purpose of the (abstract) factory pattern.
  2. (Magic) methods like __callStatic or call_user_func_array are very tricky because a developer can use it to call every method.
  3. What would you really like to do? Implement the factory pattern OR use static one-liner methods for your MongoDB implementation?!

If the implementation of the book and author collection has different methods(lets say getName() etc..) I recommend something like this:

class BookCollection extends Collection {
    protected $collection = 'book';

    public function getName() {
        return 'Book!';
    }
}

class AuthorCollection extends Collection {
    protected $collection = 'author';

    public function getName() {
        return 'Author!';
    }
}

class Collection {
    private $adapter = null;

    public function __construct() {
        $this->getAdapter()->selectCollection($this->collection);
    }
    public function findOne($query = array(), $projection = array()) {
        $doc = $this->getAdapter()->findOne($query, $projection);
        return isset($doc) ? new Document($doc) : false;
    }

    public function getAdapter() {
        // some get/set dep.injection for mongo
        if(isset($this->adapter)) {
            return $this->adapter;
        }
        return new Mongo();
    }
}

class CollectionFactory {
    public static function build($collection) 
    {
        switch($collection) {
            case 'book':
                return new BookCollection();
                break;
            case 'author':
                return new AuthorCollection(); 
                break;
        }
        // or use reflection magic
    }
}

$bookCollection = CollectionFactory::build('book');
$bookCollection->findOne(array('name' => 'Google'));
print $bookCollection->getName(); // Book!

Edit: An example with static one-liner methods

class BookCollection extends Collection {
    protected static $name = 'book';
}

class AuthorCollection extends Collection {
    protected static $name = 'author';
}

class Collection {
    private static $adapter;

    public static function setAdapter($adapter) {
        self::$adapter = $adapter;
    }
    public static function getCollectionName() {
        $self = new static();
        return $self::$name;
    }

    public function findOne($query = array(), $projection = array()) {
        self::$adapter->selectCollection(self::getCollectionName());
        $doc = self::$adapter->findOne($query, $projection);
        return $doc;
    }
}

Collection::setAdapter(new Mongo()); //initiate mongo adapter (once)
BookCollection::findOne(array('name' => 'Google'));
AuthorCollection::findOne(array('name' => 'John'));
Bas van Dorst
  • 6,632
  • 3
  • 17
  • 12
  • ***use static one-liner methods for your MongoDB implementation** => TRUE. My concern is not about methodology. My concern is about the usage. I want other developers to use my class functions instead of using default ones. By that way i can include common behaviours into that **Collection** class without worrying about what other developers are doing. For example, i don't need to ask other developers to include change logs code after their every update code. Instead if they use Book::Update(), i can do change logs work in **update** method of Collection class. – user10 Mar 10 '15 at 05:15
  • See edit. Personally I don't prefer this way of static method usage (not very easy to unit test this code), but it works. With this structure you can easily update functions inside the Collection class. – Bas van Dorst Mar 10 '15 at 10:55
  • What would you say wrong in my implementation? **(Magic) methods like __callStatic or call_user_func_array are very tricky because a developer can use it to call every method.** - What if i restrict only few methods accessible via static call? Actually i took this implementation from Flight.php source code. – user10 Mar 10 '15 at 19:15
  • Yeah that(restrict methods) is also an option, my note about tricky __callstatic usage was applicable for your piece of code. – Bas van Dorst Mar 11 '15 at 08:48
  • Thanks for confirming this. In your first method, for every collection CollectionFactory creates new instance. I like your second implementation. I really learn it new from you. But see this [comment](http://stackoverflow.com/questions/10223658/double-colon-is-working-on-non-static-function-also#10223800). But we can add static before it. No issue. Finally, how do you see my implementation. It has any performance issues or some other problems? You mention about adding new methods to Books collection. It is still possible with my method of implementation too. – user10 Mar 11 '15 at 15:42
1

Does it make sense for Collection to extend Document? It seems to me like a Collection could have Document(s), but not be a Document... So I would say this code looks a bit tangled.

Also, with the factory method, you really want to use that to instantiate a different concrete subclass of either Document or Collection. Let's suppose you've only ever got one type of Collection for ease of conversation; then your factory class needs only focus on the different Document subclasses.

So you might have a Document class that expects a raw array representing a single document.

class Document
{
    private $_aRawDoc;
    public function __construct(array $aRawDoc)
    {
        $this->_aRawDoc = $aRawDoc;
    }

    // Common Document methods here..
}

Then specialized subclasses for given Document types

class Book extends Document
{
    // Specialized Book functions ...
}

For the factory class you'll need something that will then wrap your raw results as they are read off the cursor. PDO let's you do this out of the box (see the $className parameter of PDOStatement::fetchObject for example), but we'll need to use a decorator since PHP doesn't let us get as fancy with the Mongo extension.

class MongoCursorDecorator implements MongoCursorInterface, Iterator
{
    private $_sDocClass;         // Document class to be used
    private $_oCursor;           // Underlying MongoCursor instance
    private $_aDataObjects = []; // Concrete Document instances

    // Decorate the MongoCursor, so we can wrap the results
    public function __construct(MongoCursor $oCursor, $sDocClass)
    {
        $this->_oCursor   = $oCursor;
        $this->_sDocClass = $sDocClass;
    }

    // Delegate to most of the stock MongoCursor methods
    public function __call($sMethod, array $aParams)
    {
        return call_user_func_array([$this->_oCursor, $sMethod], $aParams);
    }

    // Wrap the raw results by our Document classes
    public function current()
    {
        $key = $this->key();
        if(!isset($this->_aDataObjects[$key]))
            $this->_aDataObjects[$key] =
                new $this->sDocClass(parent::current());
        return $this->_aDataObjects[$key];
    }
}

Now a sample of how you would query mongo for books by a given author

$m          = new MongoClient();
$db         = $m->selectDB('test');
$collection = new MongoCollection($db, 'book');

// search for author
$bookQuery = array('Author' => 'JR Tolken');
$cursor    = $collection->find($bookQuery);

// Wrap the native cursor by our Decorator
$cursor = new MongoCursorDecorator($cursor, 'Book');

foreach ($cursor as $doc) {
    var_dump($doc); // This will now be an instance of Book
}

You could tighten it up a bit with a MongoCollection subclass and you may as well have it anyway, since you'll want the findOne method decorating those raw results too.

class MongoDocCollection extends MongoCollection
{
    public function find(array $query=[], array $fields=[])
    {
        // The Document class name is based on the collection name
        $sDocClass = ucfirst($this->getName());

        $cursor = parent::find($query, $fields);
        $cursor = new MongoCursorDecorator($cursor, $sDocClass);
        return $cursor;
    }

    public function findOne(
        array $query=[], array $fields=[], array $options=[]
    ) {
        $sDocClass = ucfirst($this->getName());
        return new $sDocClass(parent::findOne($query, $fields, $options));
    }
}

Then our sample usage becomes

$m          = new MongoClient();
$db         = $m->selectDB('test');
$collection = new MongoDocCollection($db, 'book');

// search for author
$bookQuery = array('Author' => 'JR Tolken');
$cursor    = $collection->find($bookQuery);

foreach($cursor as $doc) {
    var_dump($doc); // This will now be an instance of Book
}
quickshiftin
  • 66,362
  • 10
  • 68
  • 89
  • First, thanks for your answer and your time. I understand your implementation. For me your implementation looks little complex. ***From your answer: Does it make sense for Collection to extend Document? It seems to me like a Collection could have Document(s)***. I added a new method called "find" in my collection class. When user calls find method, Collection class will return array of documents as yours. My implementation make sense now? – user10 Mar 12 '15 at 16:30
  • My pleasure, thanks for reading it! The reason it's a little complex is because PHP doesn't offer a `$className` parameter for Mongo the way it does for PDO, otherwise it's a pretty typical factory setup. Your implementation still does not make sense to me; the question to ask yourself is why would a `Collection` *be* a `Document`. I think you can do it without having `Collection extends Document`, then it would make more sense to me. Typically ORMs divide the query classes and the result classes (eg. Propel & Doctrine). Your implementation is mixing them together the way I see it. – quickshiftin Mar 12 '15 at 16:56
  • Also, I'm not sure if it's clear in my answer, but if you want specialized 'query classes' using my implementation you could subclass `MongoDocCollection`. So for books something like `BookDocCollection extends MongoDocCollection { static public function create(MongoDB $db) { return new BookDocCollection($db, 'book'); } }`. There you could have specialized query logic for the book table. The `Document` class hierarchy is for the results of the query. This way you have a clean separation of the two. – quickshiftin Mar 12 '15 at 17:07
  • You can also take a look at some open source libraries, [Mandango](http://mandango.org/doc/) is the first one that pops up. There's has a lot of extras but at a high level offers two sets of classes, one for Documents, another for 'Repositories' which is essentially what the Collections are in this thread. There is also [Mongo DB ORM](http://www.doctrine-project.org/projects/mongodb-odm.html) for an example. – quickshiftin Mar 12 '15 at 17:18
  • 1
    Up voting your answer. Let me think on your comments and i'll look at those libraries. I'll reply you back once i get more ideas. – user10 Mar 13 '15 at 03:29