potfur/stash

Stash: Mongo ODM

1.0.0-alpha 2015-08-23 11:43 UTC

This package is not auto-updated.

Last update: 2024-04-13 15:01:19 UTC


README

Scrutinizer Code Quality Code Coverage Build Status License

Stash is an object-document mapper for MongoDB written in PHP. It adds a fully transparent persistence layer while still preserving MongoDB's ease of use and way of handling data.

This means that MongoDB can be used almost in the exact same way as it would be used with arrays. The small, but important, difference here is that instead of returning plain arrays, Stash will return objects (entities). And of course, Stash not only returns entities, but it also stores them.

Example

Model definitions:

$models = new \Stash\ModelCollection();
$models->register(
    new \Stash\Model\Model(
        '\Order',
        [
            new \Stash\Converter\Type\Id(),
            new \Stash\Converter\Type\Document('customer'),
            new \Stash\Converter\Type\ArrayOf('items', Fields::TYPE_DOCUMENT)
        ]
    ),
    'order'
);

$models->register(
    new \Stash\Model\
        '\OrderItem',
        [
            new \Stash\Converter\Type\Scalar('name', Fields::TYPE_STRING),
            new \Stash\Converter\Type\Scalar('amount', Fields::TYPE_INTEGER),
            new \Stash\Converter\Type\Scalar('cost', Fields::TYPE_INTEGER)
        ]
    )
);

$models->register(
    new \Stash\Model\
        '\Voucher',
        [
            new \Stash\Converter\Type\Scalar('name', Fields::TYPE_STRING),
            new \Stash\Converter\Type\Scalar('cost', Fields::TYPE_INTEGER)
        ]
    )
);

$models->register(
    new \Stash\Model\
        '\Customer',
        [
            new \Stash\Converter\Type\Scalar('name', Fields::TYPE_STRING),
            new \Stash\Converter\Type\Document('address')
        ]
    )
);

$models->register(
    new \Stash\Model\
        '\CustomerAddress',
        [
            new \Stash\Converter\Type\Scalar('address', Fields::TYPE_STRING),
            new \Stash\Converter\Type\Scalar('city', Fields::TYPE_STRING),
            new \Stash\Converter\Type\Scalar('zip', Fields::TYPE_STRING)
        ]
    )
);

Database connection:

$types = [
    new \Stash\Converter\Type\IdType(),
    new \Stash\Converter\Type\BooleanType(),
    new \Stash\Converter\Type\IntegerType(),
    new \Stash\Converter\Type\DecimalType(),
    new \Stash\Converter\Type\StringType(),
    new \Stash\Converter\Type\DateType(),
    new \Stash\Converter\Type\ArrayType(),
    new \Stash\Converter\Type\DocumentType()
];

$proxyAdapter = new \Stash\ProxyAdapter(new \ProxyManager\Factory\LazyLoadingValueHolderFactory());
$converter = new \Stash\Converter\Converter($types);
$referencer = new \Stash\ReferenceResolver($models);
$documentConverter = new \Stash\DocumentConverter($converter, $referencer, $models, $proxyAdapter);
$eventDispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();

$connection = new \Stash\Connection(new \MongoClient(), $documentConverter, $eventDispatcher);
$connection->selectDB('test');

Entity creation and storage:

class Order
{
    private $id;
    private $customer;
    private $items;

    public function __construct($customer, $items)
    {
        $this->customer = $customer;
        $this->items = $items;
    }
}

class OrderItem
{
    private $name;
    private $amount;
    private $cost;

    public function __construct($name, $amount, $cost)
    {
        $this->name = $name;
        $this->amount = $amount;
        $this->cost = $cost;
    }
}

class Voucher
{
    private $name;
    private $discount;

    public function __construct($name, $discount)
    {
        $this->name = $name;
        $this->discount = $discount;
    }
}

class Customer
{
    private $name;
    private $address;

    public function __construct($name, CustomerAddress $address)
    {
        $this->$name = $name;
        $this->address = $address;
    }
}

class CustomerAddress
{
    private $address;
    private $city;
    private $zip;

    public function __construct($address, $city, $zip)
    {
        $this->address = $address;
        $this->city = $city;
        $this->zip = $zip;
    }
}

$order = new Order(
    new Customer('Joe Doe', new CustomerAddress('Mongo alley', 'Somewhere', '12345')),
    [
        new OrderItem('Foos', 10, 1000),
        new Voucher('Voucher', 250)
    ]
);

$connection->getCollection('order')->save($order);

And this is the stored MongoDB's semi-JSON representation. When saving objects (entities), Stash adds the _class field, where it stores the class name

{
  "_id" : ObjectId("55746f4f87dee7bc0b000033"),
  "_class" : "Order",
  "customer" : {
    "_class" : "Customer",
    "address" : {
      "_class" : "CustomerAddress",
      "address" : "Mongo alley",
      "city" : "Somewhere",
      "zip" : "12345"
    }
  },
  "items" : [
    {
      "_class" : "OrderItem",
      "name" : "Foos",
      "amount" : 10,
      "cost" : 1000
    },
    {
      "_class" : "Voucher",
      "name" : "Voucher",
      "discount" : 250
    }
  ]
}                                                     

Event subscription

Stash uses Symfonys Event Dispatcher for dispatching events.

  • find.after triggered after reading document from database and converting it to entity instance
  • persist.before is triggered before converting entity to array document
  • persist.after after document was saved (and updated with new _id if needed)
  • remove.before triggered before entity is removed from database

Each event is represented with Event entity, similar to Symfony's Event but with two methods getPayload and setPayload to manage subject entity.

$subscriber = new \Fake\EventSubscriber(
    [
        \Stash\Events::FIND_AFTER,
        \Stash\Events::PERSIST_BEFORE,
        \Stash\Events::PERSIST_AFTER,
        \Stash\Events::REMOVE_BEFORE
    ]
);
$eventDispatcher->addSubscriber($subscriber);

Configuring proxy

By default, all required proxy classes are generated at runtime. Generation uses a lot of reflection and it may cause poor performance. To prevent this, proxy generator needs to be configured to reuse generated proxies.

$config = new \ProxyManager\Configuration();
$config->setProxiesTargetDir(__DIR__ . '/generated/proxy/);
spl_autoload_register($config->getProxyAutoloader());

$proxyAdapter = new \Stash\ProxyAdapter($config);